Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-07-21 18:10:08 +00:00
parent a09c6d7e91
commit 5d41ea8c8e
117 changed files with 1276 additions and 1409 deletions

View File

@ -1,122 +0,0 @@
<script>
import { GlPopover, GlSprintf, GlButton, GlLink, GlIcon } from '@gitlab/ui';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import { helpPagePath } from '~/helpers/help_page_helper';
import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue';
export default {
components: {
GlPopover,
GlSprintf,
GlButton,
GlLink,
GlIcon,
UserCalloutDismisser,
},
inject: {
message: {
default: '',
},
observerElSelector: {
default: '',
},
observerElToggledClass: {
default: '',
},
featureName: {
default: '',
},
popoverTarget: {
default: '',
},
showAttentionIcon: {
default: false,
},
delay: {
default: 0,
},
popoverCssClass: {
default: '',
},
},
data() {
return {
showPopover: false,
popoverPlacement: this.popoverPosition(),
};
},
mounted() {
this.observeEl = document.querySelector(this.observerElSelector);
this.observer = new MutationObserver(this.callback);
this.observer.observe(this.observeEl, {
attributes: true,
});
this.callback();
window.addEventListener('resize', () => {
this.popoverPlacement = this.popoverPosition();
});
},
beforeDestroy() {
this.observer.disconnect();
},
methods: {
callback() {
if (this.showPopover) {
this.$root.$emit('bv::hide::popover');
}
setTimeout(() => this.toggleShowPopover(), this.delay);
},
toggleShowPopover() {
this.showPopover = this.observeEl.classList.contains(this.observerElToggledClass);
},
getPopoverTarget() {
return document.querySelector(this.popoverTarget);
},
popoverPosition() {
if (bp.isDesktop()) {
return 'left';
}
return 'bottom';
},
},
docsPage: helpPagePath('user/project/merge_requests/index.md', {
anchor: 'request-attention-to-a-merge-request',
}),
};
</script>
<template>
<user-callout-dismisser :feature-name="featureName">
<template #default="{ shouldShowCallout, dismiss }">
<gl-popover
v-if="shouldShowCallout"
:show-close-button="false"
:target="() => getPopoverTarget()"
:show="showPopover"
:delay="0"
triggers="manual"
:placement="popoverPlacement"
boundary="window"
no-fade
:css-classes="[popoverCssClass]"
>
<p v-for="(m, index) in message" :key="index" class="gl-mb-5">
<gl-sprintf :message="m">
<template #strong="{ content }">
<strong><gl-icon v-if="showAttentionIcon" name="attention" /> {{ content }}</strong>
</template>
</gl-sprintf>
</p>
<div class="gl-display-flex gl-align-items-center">
<gl-button size="small" variant="confirm" class="gl-mr-5" @click.prevent.stop="dismiss">
{{ __('Got it!') }}
</gl-button>
<gl-link :href="$options.docsPage" target="_blank">{{ __('Learn more') }}</gl-link>
</div>
</gl-popover>
</template>
</user-callout-dismisser>
</template>

View File

@ -1,73 +0,0 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { __ } from '~/locale';
import createDefaultClient from '~/lib/graphql';
import NavigationPopover from './components/navigation_popover.vue';
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
export const initTopNavPopover = () => {
const el = document.getElementById('js-need-attention-nav-onboarding');
if (!el) return;
// eslint-disable-next-line no-new
new Vue({
el,
apolloProvider,
provide: {
observerElSelector: '.user-counter.dropdown',
observerElToggledClass: 'show',
message: [
__(
'%{strongStart}Need your attention%{strongEnd} are the merge requests that need your help to move forward, as an assignee or reviewer.',
),
],
featureName: 'attention_requests_top_nav',
popoverTarget: '#js-need-attention-nav',
},
render(h) {
return h(NavigationPopover);
},
});
};
export const initSideNavPopover = () => {
const el = document.getElementById('js-need-attention-sidebar-onboarding');
if (!el) return;
// eslint-disable-next-line no-new
new Vue({
el,
apolloProvider,
provide: {
observerElSelector: '.js-right-sidebar',
observerElToggledClass: 'right-sidebar-expanded',
message: [
__(
'To ask someone to look at a merge request, select %{strongStart}Request attention%{strongEnd}. Select again to remove the request.',
),
__(
'Some actions remove attention requests, like a reviewer approving or anyone merging the merge request.',
),
],
featureName: 'attention_requests_side_nav',
popoverTarget: '.js-attention-request-toggle',
showAttentionIcon: true,
delay: 500,
popoverCssClass: 'attention-request-sidebar-popover',
},
render(h) {
return h(NavigationPopover);
},
});
};
export default () => {
initTopNavPopover();
};

View File

@ -26,39 +26,20 @@ function updateMergeRequestCounts(newCount) {
mergeRequestsCountEl.classList.toggle('gl-display-none', Number(newCount) === 0);
}
function updateAttentionRequestsCount(count) {
const attentionCountEl = document.querySelector('.js-attention-count');
attentionCountEl.textContent = count.toLocaleString();
if (Number(count) === 0) {
attentionCountEl.classList.replace('badge-warning', 'badge-neutral');
} else {
attentionCountEl.classList.replace('badge-neutral', 'badge-warning');
}
}
/**
* Refresh user counts (and broadcast if open)
*/
export function refreshUserMergeRequestCounts() {
return getUserCounts()
.then(({ data }) => {
const attentionRequestsEnabled = window.gon?.features?.mrAttentionRequests;
const assignedMergeRequests = data.assigned_merge_requests;
const reviewerMergeRequests = data.review_requested_merge_requests;
const attentionRequests = data.attention_requests;
const fullCount = attentionRequestsEnabled
? attentionRequests
: assignedMergeRequests + reviewerMergeRequests;
const fullCount = assignedMergeRequests + reviewerMergeRequests;
updateUserMergeRequestCounts(assignedMergeRequests);
updateReviewerMergeRequestCounts(reviewerMergeRequests);
updateMergeRequestCounts(fullCount);
broadcastCount(fullCount);
if (attentionRequestsEnabled) {
updateAttentionRequestsCount(attentionRequests);
}
})
.catch((ex) => {
console.error(ex); // eslint-disable-line no-console

View File

@ -13,21 +13,6 @@ export default (IssuableTokenKeys, disableTargetBranchFilter = false) => {
IssuableTokenKeys.tokenKeys.splice(2, 0, reviewerToken);
IssuableTokenKeys.tokenKeysWithAlternative.splice(2, 0, reviewerToken);
if (window.gon?.features?.mrAttentionRequests) {
const attentionRequestedToken = {
formattedKey: __('Attention'),
key: 'attention',
type: 'string',
param: '',
symbol: '@',
icon: 'user',
tag: '@attention',
hideNotEqual: true,
};
IssuableTokenKeys.tokenKeys.splice(2, 0, attentionRequestedToken);
IssuableTokenKeys.tokenKeysWithAlternative.splice(2, 0, attentionRequestedToken);
}
const draftToken = {
token: {
formattedKey: __('Draft'),

View File

@ -276,8 +276,6 @@ class GfmAutoComplete {
UNASSIGN_REVIEWER: '/unassign_reviewer',
REASSIGN: '/reassign',
CC: '/cc',
ATTENTION: '/attention',
REMOVE_ATTENTION: '/remove_attention',
};
let assignees = [];
let reviewers = [];
@ -356,23 +354,6 @@ class GfmAutoComplete {
} else if (command === MEMBER_COMMAND.UNASSIGN_REVIEWER) {
// Only include members which are not assigned as a reviewer to Issuable currently
return data.filter((member) => reviewers.includes(member.search));
} else if (
command === MEMBER_COMMAND.ATTENTION ||
command === MEMBER_COMMAND.REMOVE_ATTENTION
) {
const attentionUsers = [
...(SidebarMediator.singleton?.store?.assignees || []),
...(SidebarMediator.singleton?.store?.reviewers || []),
];
const attentionRequested = command === MEMBER_COMMAND.REMOVE_ATTENTION;
return data.filter((member) =>
attentionUsers.find(
(u) =>
createMemberSearchString(u).includes(member.search) &&
u.attention_requested === attentionRequested,
),
);
}
return data;

View File

@ -134,12 +134,6 @@ function deferredInitialisation() {
// Adding a helper class to activate animations only after all is rendered
setTimeout(() => $body.addClass('page-initialised'), 1000);
if (window.gon?.features?.mrAttentionRequests) {
import('~/attention_requests')
.then((module) => module.default())
.catch(() => {});
}
}
// header search vue component bootstrap

View File

@ -39,9 +39,6 @@ export default {
assignSelf() {
this.$emit('assign-self');
},
toggleAttentionRequested(data) {
this.$emit('toggle-attention-requested', data);
},
},
};
</script>
@ -66,12 +63,7 @@ export default {
</template>
</span>
<uncollapsed-assignee-list
v-else
:users="sortedAssigness"
:issuable-type="issuableType"
@toggle-attention-requested="toggleAttentionRequested"
/>
<uncollapsed-assignee-list v-else :users="sortedAssigness" :issuable-type="issuableType" />
</div>
</div>
</template>

View File

@ -32,11 +32,6 @@ export default {
return this.users.length === 0;
},
},
methods: {
toggleAttentionRequested(data) {
this.$emit('toggle-attention-requested', data);
},
},
};
</script>
@ -66,7 +61,6 @@ export default {
:users="users"
:issuable-type="issuableType"
class="gl-text-gray-800 hide-collapsed"
@toggle-attention-requested="toggleAttentionRequested"
/>
</div>
</template>

View File

@ -125,9 +125,6 @@ export default {
availability: this.assigneeAvailabilityStatus[username] || '',
}));
},
toggleAttentionRequested(data) {
this.mediator.toggleAttentionRequested('assignee', data);
},
},
};
</script>
@ -155,7 +152,6 @@ export default {
:editable="store.editable"
:issuable-type="issuableType"
@assign-self="assignSelf"
@toggle-attention-requested="toggleAttentionRequested"
/>
</div>
</template>

View File

@ -2,7 +2,6 @@
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { IssuableType } from '~/issues/constants';
import { __, sprintf } from '~/locale';
import AttentionRequestedToggle from '../attention_requested_toggle.vue';
import AssigneeAvatarLink from './assignee_avatar_link.vue';
import UserNameWithStatus from './user_name_with_status.vue';
@ -10,7 +9,6 @@ const DEFAULT_RENDER_COUNT = 5;
export default {
components: {
AttentionRequestedToggle,
AssigneeAvatarLink,
UserNameWithStatus,
},
@ -46,10 +44,6 @@ export default {
return this.users.length - DEFAULT_RENDER_COUNT;
},
uncollapsedUsers() {
if (this.showVerticalList) {
return this.users;
}
const uncollapsedLength = this.showLess
? Math.min(this.users.length, DEFAULT_RENDER_COUNT)
: this.users.length;
@ -58,9 +52,6 @@ export default {
username() {
return `@${this.firstUser.username}`;
},
showVerticalList() {
return this.glFeatures.mrAttentionRequests && this.isMergeRequest;
},
isMergeRequest() {
return this.issuableType === IssuableType.MergeRequest;
},
@ -75,9 +66,6 @@ export default {
}
return u?.status?.availability || '';
},
toggleAttentionRequested(data) {
this.$emit('toggle-attention-requested', data);
},
},
};
</script>
@ -96,7 +84,7 @@ export default {
<assignee-avatar-link
:user="user"
:issuable-type="issuableType"
:tooltip-has-name="!showVerticalList"
:tooltip-has-name="!isMergeRequest"
class="gl-word-break-word"
data-css-area="user"
>
@ -107,14 +95,6 @@ export default {
<user-name-with-status :name="user.name" :availability="userAvailability(user)" />
</div>
</assignee-avatar-link>
<attention-requested-toggle
v-if="showVerticalList"
:user="user"
type="assignee"
class="gl-mr-2"
data-css-area="attention"
@toggle-attention-requested="toggleAttentionRequested"
/>
</div>
</div>
<div v-if="renderShowMoreSection" class="user-list-more gl-hover-text-blue-800">

View File

@ -1,105 +0,0 @@
<script>
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
import { __ } from '~/locale';
import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
export default {
i18n: {
addAttentionRequest: __('Add attention request'),
removeAttentionRequest: __('Remove attention request'),
attentionRequestedNoPermission: __('Attention requested'),
noAttentionRequestedNoPermission: __('No attention request'),
},
components: {
GlButton,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
type: {
type: String,
required: true,
},
user: {
type: Object,
required: true,
},
},
data() {
return {
loading: false,
};
},
computed: {
tooltipTitle() {
if (this.user.attention_requested) {
if (this.user.can_update_merge_request) {
return this.$options.i18n.removeAttentionRequest;
}
return this.$options.i18n.attentionRequestedNoPermission;
}
if (this.user.can_update_merge_request) {
return this.$options.i18n.addAttentionRequest;
}
return this.$options.i18n.noAttentionRequestedNoPermission;
},
request() {
const state = {
selected: false,
icon: 'attention',
direction: 'add',
};
if (this.user.attention_requested) {
Object.assign(state, {
selected: true,
icon: 'attention-solid',
direction: 'remove',
});
}
return state;
},
},
methods: {
toggleAttentionRequired() {
if (this.loading || !this.user.can_update_merge_request) return;
this.$root.$emit(BV_HIDE_TOOLTIP);
this.loading = true;
this.$emit('toggle-attention-requested', {
user: this.user,
callback: this.toggleAttentionRequiredComplete,
direction: this.request.direction,
});
},
toggleAttentionRequiredComplete() {
this.loading = false;
},
},
};
</script>
<template>
<div>
<span
v-gl-tooltip.left.viewport="tooltipTitle"
class="gl-display-inline-block js-attention-request-toggle"
>
<gl-button
:loading="loading"
:selected="request.selected"
:icon="request.icon"
:aria-label="tooltipTitle"
:class="{ 'gl-pointer-events-none': !user.can_update_merge_request }"
size="small"
category="tertiary"
@click="toggleAttentionRequired"
/>
</span>
</div>
</template>

View File

@ -49,9 +49,6 @@ export default {
requestReview(data) {
this.$emit('request-review', data);
},
toggleAttentionRequested(data) {
this.$emit('toggle-attention-requested', data);
},
},
};
</script>
@ -73,7 +70,6 @@ export default {
:root-path="rootPath"
:issuable-type="issuableType"
@request-review="requestReview"
@toggle-attention-requested="toggleAttentionRequested"
/>
</div>
</div>

View File

@ -88,9 +88,6 @@ export default {
requestReview(data) {
this.mediator.requestReview(data);
},
toggleAttentionRequested(data) {
this.mediator.toggleAttentionRequested('reviewer', data);
},
},
};
</script>
@ -109,7 +106,6 @@ export default {
:editable="store.editable"
:issuable-type="issuableType"
@request-review="requestReview"
@toggle-attention-requested="toggleAttentionRequested"
/>
</div>
</template>

View File

@ -1,8 +1,6 @@
<script>
import { GlButton, GlTooltipDirective, GlIcon } from '@gitlab/ui';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { __, sprintf, s__ } from '~/locale';
import AttentionRequestedToggle from '../attention_requested_toggle.vue';
import ReviewerAvatarLink from './reviewer_avatar_link.vue';
const LOADING_STATE = 'loading';
@ -16,12 +14,10 @@ export default {
GlButton,
GlIcon,
ReviewerAvatarLink,
AttentionRequestedToggle,
},
directives: {
GlTooltip: GlTooltipDirective,
},
mixins: [glFeatureFlagsMixin()],
props: {
users: {
type: Array,
@ -80,9 +76,6 @@ export default {
this.loadingStates[userId] = null;
}
},
toggleAttentionRequested(data) {
this.$emit('toggle-attention-requested', data);
},
},
LOADING_STATE,
SUCCESS_STATE,
@ -96,7 +89,6 @@ export default {
:key="user.id"
:class="{
'gl-mb-3': index !== users.length - 1,
'attention-requests': glFeatures.mrAttentionRequests,
}"
class="gl-display-grid gl-align-items-center reviewer-grid gl-mr-2"
data-testid="reviewer"
@ -112,14 +104,6 @@ export default {
{{ user.name }}
</div>
</reviewer-avatar-link>
<attention-requested-toggle
v-if="glFeatures.mrAttentionRequests"
:user="user"
type="reviewer"
class="gl-mr-2"
data-css-area="attention"
@toggle-attention-requested="toggleAttentionRequested"
/>
<gl-icon
v-if="user.approved"
v-gl-tooltip.left
@ -137,9 +121,7 @@ export default {
data-testid="re-request-success"
/>
<gl-button
v-else-if="
user.can_update_merge_request && user.reviewed && !glFeatures.mrAttentionRequests
"
v-else-if="user.can_update_merge_request && user.reviewed"
v-gl-tooltip.left
:title="$options.i18n.reRequestReview"
:aria-label="$options.i18n.reRequestReview"

View File

@ -27,8 +27,6 @@ import trackShowInviteMemberLink from '~/sidebar/track_invite_members';
import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_vue/constants';
import LabelsSelectWidget from '~/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue';
import { LabelType } from '~/vue_shared/components/sidebar/labels_select_widget/constants';
import eventHub from '~/sidebar/event_hub';
import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
import Translate from '../vue_shared/translate';
import SidebarAssignees from './components/assignees/sidebar_assignees.vue';
import CopyEmailToClipboard from './components/copy_email_to_clipboard.vue';
@ -652,13 +650,6 @@ export function mountSidebar(mediator, store) {
mountSeverityComponent();
mountEscalationStatusComponent();
if (window.gon?.features?.mrAttentionRequests) {
eventHub.$on('removeCurrentUserAttentionRequested', () => {
mediator.removeCurrentUserAttentionRequested();
refreshUserMergeRequestCounts();
});
}
}
export { getSidebarOptions };

View File

@ -1,7 +0,0 @@
mutation mergeRequestRemoveAttentionRequest($projectPath: ID!, $iid: String!, $userId: UserID!) {
mergeRequestRemoveAttentionRequest(
input: { projectPath: $projectPath, iid: $iid, userId: $userId }
) {
errors
}
}

View File

@ -1,5 +0,0 @@
mutation mergeRequestRequestAttention($projectPath: ID!, $iid: String!, $userId: UserID!) {
mergeRequestRequestAttention(input: { projectPath: $projectPath, iid: $iid, userId: $userId }) {
errors
}
}

View File

@ -5,8 +5,6 @@ import createGqClient, { fetchPolicies } from '~/lib/graphql';
import axios from '~/lib/utils/axios_utils';
import reviewerRereviewMutation from '../queries/reviewer_rereview.mutation.graphql';
import sidebarDetailsMRQuery from '../queries/sidebar_details_mr.query.graphql';
import requestAttentionMutation from '../queries/request_attention.mutation.graphql';
import removeAttentionRequestMutation from '../queries/remove_attention_request.mutation.graphql';
const queries = {
merge_request: sidebarDetailsMRQuery,
@ -93,25 +91,4 @@ export default class SidebarService {
},
});
}
requestAttention(userId) {
return gqClient.mutate({
mutation: requestAttentionMutation,
variables: {
userId: convertToGraphQLId(TYPE_USER, `${userId}`),
projectPath: this.fullPath,
iid: this.iid.toString(),
},
});
}
removeAttentionRequest(userId) {
return gqClient.mutate({
mutation: removeAttentionRequestMutation,
variables: {
userId: convertToGraphQLId(TYPE_USER, `${userId}`),
projectPath: this.fullPath,
iid: this.iid.toString(),
},
});
}
}

View File

@ -3,17 +3,7 @@ import Mediator from './sidebar_mediator';
export default (store) => {
const mediator = new Mediator(getSidebarOptions());
mediator
.fetch()
.then(() => {
if (window.gon?.features?.mrAttentionRequests) {
return import('~/attention_requests');
}
return null;
})
.then((module) => module?.initSideNavPopover())
.catch(() => {});
mediator.fetch();
mountSidebar(mediator, store);
};

View File

@ -1,8 +1,7 @@
import Store from '~/sidebar/stores/sidebar_store';
import createFlash from '~/flash';
import { __, sprintf } from '~/locale';
import { __ } from '~/locale';
import toast from '~/vue_shared/plugins/global_toast';
import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
import { visitUrl } from '../lib/utils/url_utility';
import Service from './services/sidebar_service';
@ -42,7 +41,6 @@ export default class SidebarMediator {
const data = { assignee_ids: assignees };
try {
const { currentUserHasAttention } = this.store;
const res = await this.service.update(field, data);
this.store.overwrite('assignees', res.data.assignees);
@ -51,10 +49,6 @@ export default class SidebarMediator {
this.store.overwrite('reviewers', res.data.reviewers);
}
if (currentUserHasAttention && this.store.isAddingAssignee) {
toast(__('Assigned user(s). Your attention request was removed.'));
}
return Promise.resolve(res);
} catch (e) {
return Promise.reject(e);
@ -70,16 +64,11 @@ export default class SidebarMediator {
const data = { reviewer_ids: reviewers };
try {
const { currentUserHasAttention } = this.store;
const res = await this.service.update(field, data);
this.store.overwrite('reviewers', res.data.reviewers);
this.store.overwrite('assignees', res.data.assignees);
if (currentUserHasAttention && this.store.isAddingAssignee) {
toast(__('Requested review. Your attention request was removed.'));
}
return Promise.resolve(res);
} catch (e) {
return Promise.reject();
@ -97,80 +86,6 @@ export default class SidebarMediator {
.catch(() => callback(userId, false));
}
removeCurrentUserAttentionRequested() {
const currentUserId = gon.current_user_id;
const currentUserReviewer = this.store.findReviewer({ id: currentUserId });
const currentUserAssignee = this.store.findAssignee({ id: currentUserId });
if (currentUserReviewer?.attention_requested || currentUserAssignee?.attention_requested) {
// Update current users attention_requested state
this.store.updateReviewer(currentUserId, 'attention_requested');
this.store.updateAssignee(currentUserId, 'attention_requested');
}
}
async toggleAttentionRequested(type, { user, callback, direction }) {
const mutations = {
add: (id) => this.service.requestAttention(id),
remove: (id) => this.service.removeAttentionRequest(id),
};
try {
const isReviewer = type === 'reviewer';
const reviewerOrAssignee = isReviewer
? this.store.findReviewer(user)
: this.store.findAssignee(user);
await mutations[direction]?.(user.id);
if (reviewerOrAssignee.attention_requested) {
toast(
sprintf(__('Removed attention request from @%{username}'), {
username: user.username,
}),
);
} else {
const currentUserId = gon.current_user_id;
const { currentUserHasAttention } = this.store;
if (currentUserId !== user.id) {
this.removeCurrentUserAttentionRequested();
}
toast(
currentUserHasAttention && currentUserId !== user.id
? sprintf(
__(
'Requested attention from @%{username}. Your own attention request was removed.',
),
{ username: user.username },
)
: sprintf(__('Requested attention from @%{username}'), { username: user.username }),
);
}
this.store.updateReviewer(user.id, 'attention_requested');
this.store.updateAssignee(user.id, 'attention_requested');
refreshUserMergeRequestCounts();
callback();
} catch (error) {
callback();
createFlash({
message: sprintf(__('Updating the attention request for %{username} failed.'), {
username: user.username,
}),
error,
captureError: true,
actionConfig: {
title: __('Try again'),
clickHandler: () => this.toggleAttentionRequired(type, { user, callback, direction }),
},
});
}
}
setMoveToProjectId(projectId) {
this.store.setMoveToProjectId(projectId);
}

View File

@ -19,9 +19,7 @@ export default class SidebarStore {
this.humanTimeSpent = '';
this.timeTrackingLimitToHours = timeTrackingLimitToHours;
this.assignees = [];
this.addingAssignees = [];
this.reviewers = [];
this.addingReviewers = [];
this.isFetching = {
assignees: true,
reviewers: true,
@ -77,20 +75,12 @@ export default class SidebarStore {
if (!this.findAssignee(assignee)) {
this.changing = true;
this.assignees.push(assignee);
if (assignee.id !== this.currentUser.id) {
this.addingAssignees.push(assignee.id);
}
}
}
addReviewer(reviewer) {
if (!this.findReviewer(reviewer)) {
this.reviewers.push(reviewer);
if (reviewer.id !== this.currentUser.id) {
this.addingReviewers.push(reviewer.id);
}
}
}
@ -126,14 +116,12 @@ export default class SidebarStore {
if (assignee) {
this.changing = true;
this.assignees = this.assignees.filter(({ id }) => id !== assignee.id);
this.addingAssignees = this.addingAssignees.filter(({ id }) => id !== assignee.id);
}
}
removeReviewer(reviewer) {
if (reviewer) {
this.reviewers = this.reviewers.filter(({ id }) => id !== reviewer.id);
this.addingReviewers = this.addingReviewers.filter(({ id }) => id !== reviewer.id);
}
}
@ -161,26 +149,4 @@ export default class SidebarStore {
setMoveToProjectId(moveToProjectId) {
this.moveToProjectId = moveToProjectId;
}
get currentUserHasAttention() {
if (!window.gon?.features?.mrAttentionRequests || !this.isMergeRequest) return false;
const currentUserId = this.currentUser.id;
const currentUserReviewer = this.findReviewer({ id: currentUserId });
const currentUserAssignee = this.findAssignee({ id: currentUserId });
return currentUserReviewer?.attention_requested || currentUserAssignee?.attention_requested;
}
get isAddingAssignee() {
return this.addingAssignees.length > 0;
}
get isAddingReviewer() {
return this.addingReviewers.length > 0;
}
get isMergeRequest() {
return this.issuableType === 'merge_request';
}
}

View File

@ -4,9 +4,6 @@ import createFlash from '~/flash';
import { BV_SHOW_MODAL } from '~/lib/utils/constants';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { s__, __ } from '~/locale';
import sidebarEventHub from '~/sidebar/event_hub';
import showToast from '~/vue_shared/plugins/global_toast';
import SidebarMediator from '~/sidebar/sidebar_mediator';
import eventHub from '../../event_hub';
import approvalsMixin from '../../mixins/approvals';
import MrWidgetContainer from '../mr_widget_container.vue';
@ -192,16 +189,8 @@ export default {
.then((data) => {
this.mr.setApprovals(data);
if (
this.glFeatures.mrAttentionRequests &&
SidebarMediator.singleton?.store.currentUserHasAttention
) {
showToast(__('Approved. Your attention request was removed.'));
}
eventHub.$emit('MRWidgetUpdateRequested');
eventHub.$emit('ApprovalUpdated');
sidebarEventHub.$emit('removeCurrentUserAttentionRequested');
this.$emit('updated');
})
.catch(errFn)

View File

@ -43,7 +43,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:realtime_labels, project)
push_frontend_feature_flag(:refactor_security_extension, @project)
push_frontend_feature_flag(:refactor_code_quality_inline_findings, project)
push_frontend_feature_flag(:mr_attention_requests, current_user)
push_frontend_feature_flag(:moved_mr_sidebar, project)
push_frontend_feature_flag(:paginated_mr_discussions, project)
push_frontend_feature_flag(:mr_review_submit_comment, project)

View File

@ -15,10 +15,6 @@ module DashboardHelper
merge_requests_dashboard_path(reviewer_username: current_user.username)
end
def attention_requested_mrs_dashboard_path
merge_requests_dashboard_path(attention: current_user.username)
end
def dashboard_nav_links
@dashboard_nav_links ||= get_dashboard_nav_links
end

View File

@ -15,7 +15,7 @@ module Ci
include Presentable
include EachBatch
ignore_column :semver, remove_with: '15.3', remove_after: '2022-07-22'
ignore_column :semver, remove_with: '15.4', remove_after: '2022-08-22'
add_authentication_token_field :token, encrypted: :optional, expires_at: :compute_token_expiration, expiration_enforced?: :token_expiration_enforced?

View File

@ -331,11 +331,7 @@ class Environment < ApplicationRecord
end
def last_deployment_group
if ::Feature.enabled?(:batch_load_environment_last_deployment_group, project)
Deployment.last_deployment_group_for_environment(self)
else
legacy_last_deployment_group
end
Deployment.last_deployment_group_for_environment(self)
end
def reset_auto_stop

View File

@ -3,6 +3,20 @@
module Projects
module ImportExport
class RelationExport < ApplicationRecord
DESIGN_REPOSITORY_RELATION = 'design_repository'
LFS_OBJECTS_RELATION = 'lfs_objects'
REPOSITORY_RELATION = 'repository'
ROOT_RELATION = 'project'
SNIPPETS_REPOSITORY_RELATION = 'snippets_repository'
UPLOADS_RELATION = 'uploads'
WIKI_REPOSITORY_RELATION = 'wiki_repository'
EXTRA_RELATION_LIST = [
DESIGN_REPOSITORY_RELATION, LFS_OBJECTS_RELATION, REPOSITORY_RELATION, ROOT_RELATION,
SNIPPETS_REPOSITORY_RELATION, UPLOADS_RELATION, WIKI_REPOSITORY_RELATION
].freeze
private_constant :EXTRA_RELATION_LIST
self.table_name = 'project_relation_exports'
belongs_to :project_export_job
@ -17,6 +31,33 @@ module Projects
validates :project_export_job, presence: true
validates :relation, presence: true, length: { maximum: 255 }, uniqueness: { scope: :project_export_job_id }
validates :status, numericality: { only_integer: true }, presence: true
scope :by_relation, -> (relation) { where(relation: relation) }
state_machine :status, initial: :queued do
state :queued, value: 0
state :started, value: 1
state :finished, value: 2
state :failed, value: 3
event :start do
transition queued: :started
end
event :finish do
transition started: :finished
end
event :fail_op do
transition [:queued, :started] => :failed
end
end
def self.relation_names_list
project_tree_relation_names = ::Gitlab::ImportExport::Reader.new(shared: nil).project_relation_names.map(&:to_s)
project_tree_relation_names + EXTRA_RELATION_LIST
end
end
end
end

View File

@ -56,10 +56,6 @@ class EnvironmentSerializer < BaseSerializer
resource = resource.preload(environment_associations.except(:last_deployment, :upcoming_deployment))
if ::Feature.enabled?(:batch_load_environment_last_deployment_group, resource.first&.project)
temp_deployment_associations[:deployable][:pipeline][:latest_successful_builds] = []
end
Preloaders::Environments::DeploymentPreloader.new(resource)
.execute_with_union(:last_deployment, temp_deployment_associations)
@ -72,10 +68,8 @@ class EnvironmentSerializer < BaseSerializer
environment.last_deployment&.commit&.try(:lazy_author)
environment.upcoming_deployment&.commit&.try(:lazy_author)
if ::Feature.enabled?(:batch_load_environment_last_deployment_group, environment.project)
# Batch loading last_deployment_group which is called later by environment.stop_actions
environment.last_deployment_group
end
# Batch loading last_deployment_group which is called later by environment.stop_actions
environment.last_deployment_group
end
end
end
@ -101,7 +95,8 @@ class EnvironmentSerializer < BaseSerializer
metadata: [],
pipeline: {
manual_actions: [:metadata, :deployment],
scheduled_actions: [:metadata]
scheduled_actions: [:metadata],
latest_successful_builds: []
},
project: project_associations,
deployment: []

View File

@ -0,0 +1,95 @@
# frozen_string_literal: true
module Projects
module ImportExport
class RelationExportService
include Gitlab::ImportExport::CommandLineUtil
def initialize(relation_export, jid)
@relation_export = relation_export
@jid = jid
@logger = Gitlab::Export::Logger.build
end
def execute
relation_export.update!(status_event: :start, jid: jid)
mkdir_p(shared.export_path)
mkdir_p(shared.archive_path)
if relation_saver.save
compress_export_path
upload_compressed_file
relation_export.finish!
else
fail_export(shared.errors.join(', '))
end
rescue StandardError => e
fail_export(e.message)
ensure
FileUtils.remove_entry(shared.export_path) if File.exist?(shared.export_path)
FileUtils.remove_entry(shared.archive_path) if File.exist?(shared.archive_path)
end
private
attr_reader :relation_export, :jid, :logger
delegate :relation, :project_export_job, to: :relation_export
delegate :project, to: :project_export_job
def shared
project.import_export_shared
end
def relation_saver
case relation
when Projects::ImportExport::RelationExport::UPLOADS_RELATION
Gitlab::ImportExport::UploadsSaver.new(project: project, shared: shared)
when Projects::ImportExport::RelationExport::REPOSITORY_RELATION
Gitlab::ImportExport::RepoSaver.new(exportable: project, shared: shared)
when Projects::ImportExport::RelationExport::WIKI_REPOSITORY_RELATION
Gitlab::ImportExport::WikiRepoSaver.new(exportable: project, shared: shared)
when Projects::ImportExport::RelationExport::LFS_OBJECTS_RELATION
Gitlab::ImportExport::LfsSaver.new(project: project, shared: shared)
when Projects::ImportExport::RelationExport::SNIPPETS_REPOSITORY_RELATION
Gitlab::ImportExport::SnippetsRepoSaver.new(project: project, shared: shared, current_user: nil)
when Projects::ImportExport::RelationExport::DESIGN_REPOSITORY_RELATION
Gitlab::ImportExport::DesignRepoSaver.new(exportable: project, shared: shared)
else
Gitlab::ImportExport::Project::RelationSaver.new(
project: project,
shared: shared,
relation: relation
)
end
end
def upload_compressed_file
upload = relation_export.build_upload
File.open(archive_file_full_path) { |file| upload.export_file = file }
upload.save!
end
def compress_export_path
tar_czf(archive: archive_file_full_path, dir: shared.export_path)
end
def archive_file_full_path
@archive_file ||= File.join(shared.archive_path, "#{relation}.tar.gz")
end
def fail_export(error_message)
relation_export.update!(status_event: :fail_op, export_error: error_message.truncate(300))
logger.error(
message: 'Project relation export failed',
export_error: error_message,
project_export_job_id: project_export_job.id,
project_name: project.name,
project_id: project.id
)
end
end
end
end

View File

@ -18,7 +18,7 @@ module WorkItems
create_result = CreateService.new(
project: @project,
current_user: @current_user,
params: @params.reverse_merge(confidential: confidential_parent),
params: @params.merge(title: @params[:title].strip).reverse_merge(confidential: confidential_parent),
spam_params: @spam_params
).execute
return create_result if create_result.error?

View File

@ -57,7 +57,7 @@
= number_with_delimiter(issues_count)
- if header_link?(:merge_requests)
= nav_link(path: 'dashboard#merge_requests', html_options: { class: "user-counter dropdown" }) do
- top_level_link = current_user.mr_attention_requests_enabled? ? attention_requested_mrs_dashboard_path : assigned_mrs_dashboard_path
- top_level_link = assigned_mrs_dashboard_path
= link_to top_level_link, class: 'dashboard-shortcuts-merge_requests', title: _('Merge requests'), aria: { label: _('Merge requests') },
data: { qa_selector: 'merge_requests_shortcut_button',
toggle: "dropdown",
@ -74,27 +74,14 @@
%ul
%li.dropdown-header
= _('Merge requests')
- if current_user.mr_attention_requests_enabled?
%li#js-need-attention-nav
#js-need-attention-nav-onboarding
= link_to attention_requested_mrs_dashboard_path, class: 'gl-display-flex! gl-align-items-center js-prefetch-document' do
= _('Need your attention')
= gl_badge_tag user_merge_requests_counts[:attention_requested_count], { size: :sm, variant: user_merge_requests_counts[:attention_requested_count] == 0 ? :neutral : :warning }, { class: 'merge-request-badge gl-ml-auto js-attention-count' }
%li.divider
%li
= link_to assigned_mrs_dashboard_path, class: 'gl-display-flex! gl-align-items-center js-prefetch-document' do
- if current_user.mr_attention_requests_enabled?
= _('Assignee')
- else
= _('Assigned to you')
= _('Assigned to you')
= gl_badge_tag({ variant: :neutral, size: :sm }, { class: "js-assigned-mr-count gl-ml-auto" }) do
= user_merge_requests_counts[:assigned]
%li
= link_to reviewer_mrs_dashboard_path, class: 'gl-display-flex! gl-align-items-center js-prefetch-document' do
- if current_user.mr_attention_requests_enabled?
= _('Reviewer')
- else
= _('Review requests for you')
= _('Review requests for you')
= gl_badge_tag({ variant: :neutral, size: :sm }, { class: "js-reviewer-mr-count gl-ml-auto" }) do
= user_merge_requests_counts[:review_requested]
- if header_link?(:todos)

View File

@ -102,8 +102,5 @@
- if Feature.enabled?(:mr_experience_survey, @project)
#js-mr-experience-survey
- if current_user&.mr_attention_requests_enabled?
#js-need-attention-sidebar-onboarding
= render 'projects/invite_members_modal', project: @project
= render 'shared/web_ide_path'

View File

@ -3,11 +3,8 @@
- render_count = assignees_rendering_overflow ? max_render - 1 : max_render
- more_assignees_count = issuable.assignees.size - render_count
- if issuable.instance_of?(MergeRequest) && current_user&.mr_attention_requests_enabled?
= render 'shared/issuable/merge_request_assignees', issuable: issuable, count: render_count
- else
- issuable.assignees.take(render_count).each do |assignee| # rubocop: disable CodeReuse/ActiveRecord
= link_to_member(@project, assignee, name: false, title: s_("MrList|Assigned to %{name}") % { name: assignee.name})
- issuable.assignees.take(render_count).each do |assignee| # rubocop: disable CodeReuse/ActiveRecord
= link_to_member(@project, assignee, name: false, title: s_("MrList|Assigned to %{name}") % { name: assignee.name})
- if more_assignees_count > 0
%span{ class: 'avatar-counter has-tooltip', data: { container: 'body', placement: 'bottom', 'line-type' => 'old', qa_selector: 'avatar_counter_content' }, title: _("+%{more_assignees_count} more assignees") % { more_assignees_count: more_assignees_count} }

View File

@ -1,8 +0,0 @@
- issuable.merge_request_assignees.take(count).each do |merge_request_assignee| # rubocop: disable CodeReuse/ActiveRecord
- assignee = merge_request_assignee.assignee
- assignee_tooltip = ( merge_request_assignee.attention_requested? ? s_("MrList|Attention requested from assignee %{name}") : s_("MrList|Assigned to %{name}") ) % { name: assignee.name}
= link_to_member(@project, assignee, name: false, title: assignee_tooltip, extra_class: "gl-flex-direction-row-reverse") do
- if merge_request_assignee.attention_requested?
%span.gl-display-inline-flex
= sprite_icon('attention-solid-sm', css_class: 'gl-text-orange-500 icon-overlap-and-shadow')

View File

@ -1,8 +0,0 @@
- issuable.merge_request_reviewers.take(count).each do |merge_request_reviewer| # rubocop: disable CodeReuse/ActiveRecord
- reviewer = merge_request_reviewer.reviewer
- reviewer_tooltip = ( merge_request_reviewer.attention_requested? ? s_("MrList|Attention requested from reviewer %{name}") : s_("MrList|Review requested from %{name}") ) % { name: reviewer.name}
= link_to_member(@project, reviewer, name: false, title: reviewer_tooltip, extra_class: "gl-flex-direction-row-reverse") do
- if merge_request_reviewer.attention_requested?
%span.gl-display-inline-flex
= sprite_icon('attention-solid-sm', css_class: 'gl-text-orange-500 icon-overlap-and-shadow')

View File

@ -3,11 +3,8 @@
- render_count = reviewers_rendering_overflow ? max_render - 1 : max_render
- more_reviewers_count = issuable.reviewers.size - render_count
- if issuable.instance_of?(MergeRequest) && current_user&.mr_attention_requests_enabled?
= render 'shared/issuable/merge_request_reviewers', issuable: issuable, count: render_count
- else
- issuable.reviewers.take(render_count).each do |reviewer| # rubocop: disable CodeReuse/ActiveRecord
= link_to_member(@project, reviewer, name: false, title: s_("MrList|Review requested from %{name}") % { name: reviewer.name})
- issuable.reviewers.take(render_count).each do |reviewer| # rubocop: disable CodeReuse/ActiveRecord
= link_to_member(@project, reviewer, name: false, title: s_("MrList|Review requested from %{name}") % { name: reviewer.name})
- if more_reviewers_count > 0
%span{ class: 'avatar-counter has-tooltip', data: { container: 'body', placement: 'bottom', 'line-type' => 'old' }, title: _("+%{more_reviewers_count} more reviewers") % { more_reviewers_count: more_reviewers_count} }

View File

@ -88,16 +88,6 @@
= render 'shared/issuable/user_dropdown_item',
user: User.new(username: '{{username}}', name: '{{name}}'),
avatar: { lazy: true, url: '{{avatar_url}}' }
- if current_user&.mr_attention_requests_enabled?
#js-dropdown-attention-requested.filtered-search-input-dropdown-menu.dropdown-menu
- if current_user
%ul{ data: { dropdown: true } }
= render 'shared/issuable/user_dropdown_item',
user: current_user
%ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
= render 'shared/issuable/user_dropdown_item',
user: User.new(username: '{{username}}', name: '{{name}}'),
avatar: { lazy: true, url: '{{avatar_url}}' }
= render_if_exists 'shared/issuable/approver_dropdown'
= render_if_exists 'shared/issuable/approved_by_dropdown'
#js-dropdown-milestone.filtered-search-input-dropdown-menu.dropdown-menu

View File

@ -2812,6 +2812,15 @@
:weight: 1
:idempotent: false
:tags: []
- :name: projects_import_export_relation_export
:worker_name: Projects::ImportExport::RelationExportWorker
:feature_category: :importers
:has_external_dependencies: false
:urgency: :low
:resource_boundary: :memory
:weight: 1
:idempotent: true
:tags: []
- :name: projects_inactive_projects_deletion_notification
:worker_name: Projects::InactiveProjectsDeletionNotificationWorker
:feature_category: :compliance_management

View File

@ -0,0 +1,26 @@
# frozen_string_literal: true
module Projects
module ImportExport
class RelationExportWorker
include ApplicationWorker
include ExceptionBacktrace
idempotent!
data_consistency :always
deduplicate :until_executed
feature_category :importers
sidekiq_options status_expiration: StuckExportJobsWorker::EXPORT_JOBS_EXPIRATION
urgency :low
worker_resource_boundary :memory
def perform(project_relation_export_id)
relation_export = Projects::ImportExport::RelationExport.find(project_relation_export_id)
if relation_export.queued?
Projects::ImportExport::RelationExportService.new(relation_export, jid).execute
end
end
end
end
end

View File

@ -1,8 +0,0 @@
---
name: batch_load_environment_last_deployment_group
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/86584/
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/363023
milestone: '15.1'
type: development
group: group::release
default_enabled: true

View File

@ -363,6 +363,8 @@
- 1
- - projects_git_garbage_collect
- 1
- - projects_import_export_relation_export
- 1
- - projects_inactive_projects_deletion_notification
- 1
- - projects_post_creation

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
class DropQueuedAtIndexFromCiBuilds < Gitlab::Database::Migration[2.0]
disable_ddl_transaction!
INDEX_NAME = 'index_ci_builds_on_queued_at'
def up
remove_concurrent_index_by_name :ci_builds, INDEX_NAME
end
# rubocop:disable Migration/PreventIndexCreation
def down
add_concurrent_index :ci_builds, :queued_at, name: INDEX_NAME
end
# rubocop:enable Migration/PreventIndexCreation
end

View File

@ -0,0 +1,21 @@
# frozen_string_literal: true
class RemoveCiRunnersSemverColumn < Gitlab::Database::Migration[2.0]
disable_ddl_transaction!
INDEX_NAME = 'index_ci_runners_on_id_and_semver_cidr'
def up
with_lock_retries do
remove_column :ci_runners, :semver
end
end
def down
with_lock_retries do
add_column :ci_runners, :semver, :text, null: true
end
add_text_limit :ci_runners, :semver, 16
add_concurrent_index :ci_runners, 'id, (semver::cidr)', name: INDEX_NAME
end
end

View File

@ -0,0 +1 @@
2d5bf23684afbd4dbf3251c4886c22eaaa144332901c1183bc474772f065c54f

View File

@ -0,0 +1 @@
c9b214fd49c97d17f43faef4d86b811ea2ad5f573c3cb4a6725de8ee4c92262a

View File

@ -13154,8 +13154,6 @@ CREATE TABLE ci_runners (
maintainer_note text,
token_expires_at timestamp with time zone,
allowed_plans text[] DEFAULT '{}'::text[] NOT NULL,
semver text,
CONSTRAINT check_a4f24953fd CHECK ((char_length(semver) <= 16)),
CONSTRAINT check_ce275cee06 CHECK ((char_length(maintainer_note) <= 1024))
);
@ -27537,8 +27535,6 @@ CREATE INDEX index_ci_builds_on_project_id_and_id ON ci_builds USING btree (proj
CREATE INDEX index_ci_builds_on_project_id_and_name_and_ref ON ci_builds USING btree (project_id, name, ref) WHERE (((type)::text = 'Ci::Build'::text) AND ((status)::text = 'success'::text) AND ((retried = false) OR (retried IS NULL)));
CREATE INDEX index_ci_builds_on_queued_at ON ci_builds USING btree (queued_at);
CREATE INDEX index_ci_builds_on_resource_group_and_status_and_commit_id ON ci_builds USING btree (resource_group_id, status, commit_id) WHERE (resource_group_id IS NOT NULL);
CREATE INDEX index_ci_builds_on_runner_id_and_id_desc ON ci_builds USING btree (runner_id, id DESC);
@ -27747,8 +27743,6 @@ CREATE INDEX index_ci_runners_on_created_at_desc_and_id_desc ON ci_runners USING
CREATE INDEX index_ci_runners_on_description_trigram ON ci_runners USING gin (description gin_trgm_ops);
CREATE INDEX index_ci_runners_on_id_and_semver_cidr ON ci_runners USING btree (id, ((semver)::cidr));
CREATE INDEX index_ci_runners_on_locked ON ci_runners USING btree (locked);
CREATE INDEX index_ci_runners_on_runner_type ON ci_runners USING btree (runner_type);

View File

@ -700,7 +700,7 @@ Example of response
"stage": "test",
"status": "canceled",
"tag": false,
"web_url": "https://example.com/foo/bar/-/jobs/42",
"web_url": "https://example.com/foo/bar/-/jobs/1",
"user": null
}
```
@ -750,7 +750,7 @@ Example of response
"stage": "test",
"status": "pending",
"tag": false,
"web_url": "https://example.com/foo/bar/-/jobs/42",
"web_url": "https://example.com/foo/bar/-/jobs/1",
"user": null
}
```
@ -805,7 +805,7 @@ Example of response
"queued_duration": 0.010,
"status": "failed",
"tag": false,
"web_url": "https://example.com/foo/bar/-/jobs/42",
"web_url": "https://example.com/foo/bar/-/jobs/1",
"user": null
}
```
@ -881,7 +881,7 @@ Example response:
"stage": "test",
"status": "pending",
"tag": false,
"web_url": "https://example.com/foo/bar/-/jobs/42",
"web_url": "https://example.com/foo/bar/-/jobs/1",
"user": null
}
```

View File

@ -39,7 +39,7 @@ GET /users
]
```
You can also search for users by name, username or public email by using `?search=`. For example. `/users?search=John`.
You can also search for users by name, username, or public email by using `?search=`. For example. `/users?search=John`.
In addition, you can lookup users by username:
@ -1220,7 +1220,7 @@ Parameters:
curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/user/gpg_keys/1"
```
Returns `204 No Content` on success, or `404 Not found` if the key cannot be found.
Returns `204 No Content` on success or `404 Not Found` if the key cannot be found.
## List all GPG keys for given user
@ -1964,7 +1964,7 @@ Pre-requisite:
- You must be an administrator.
Lists all projects and groups a user is a member of.
It returns the `source_id`, `source_name`, `source_type` and `access_level` of a membership.
It returns the `source_id`, `source_name`, `source_type`, and `access_level` of a membership.
Source can be of type `Namespace` (representing a group) or `Project`. The response represents only direct memberships. Inherited memberships, for example in subgroups, are not included.
Access levels are represented by an integer value. For more details, read about the meaning of [access level values](access_requests.md#valid-access-levels).

View File

@ -0,0 +1,156 @@
---
stage: Data Stores
group: Database
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# CI mirrored tables
## Problem statement
As part of the database [decomposition work](https://gitlab.com/groups/gitlab-org/-/epics/6168),
which had the goal of splitting the single database GitLab is using, into two databases: `main` and
`ci`, came the big challenge of removing all joins between the tables
[that don't reside on the same database](multiple_databases.md#removing-joins-between-ci-and-non-ci-tables).
PostgreSQL doesn't support joins between tables that belong to different databases. However,
some core application models in the main database are queried very often by the CI side.
For example:
- `Namespace`, in the `namespaces` table.
- `Project`, in the `projects` table.
Not being able to do `joins` on these tables brings a great challenge. The team chose to perform logical
replication of those tables from the main database to the CI database, in the new tables:
- `ci_namespace_mirrors`, as a mirror of the `namespaces` table
- `ci_project_mirrors`, as a mirror of the `projects` table
This logical replication means two things:
1. The `main` database tables can be queried and joined to the `namespaces` and `projects` tables.
1. The `ci` database tables can be joined with the `ci_namespace_mirrors` and `ci_project_mirrors` tables.
```mermaid
graph LR
subgraph "Main database (tables)"
A[namespaces] -->|updates| B[namespaces_sync_events]
A -->|deletes| C[loose_foreign_keys_deleted_records]
D[projects] -->|deletes| C
D -->|updates| E[projects_sync_events]
end
B --> F
C --> G
E --> H
subgraph "Sidekiq worker jobs"
F[Namespaces::ProcessSyncEventsWorker]
G[LooseForeignKeys::CleanupWorker]
H[Projects::ProcessSyncEventsWorker]
end
F -->|do update| I
G -->|delete records| I
G -->|delete records| J
H -->|do update| J
subgraph "CI database (tables)"
I[ci_namespace_mirrors]
J[ci_project_mirrors]
end
```
This replication was restricted only to a few attributes that are needed from each model:
- From `Namespace` we replicate `traversal_ids`.
- From `Project` we replicate only the `namespace_id`, which represents the group which the project belongs to.
## Keeping the CI mirrored tables in sync with the source tables
We must care about two type 3 events to keep
the source and the target tables in sync:
1. Creation of new namespaces or projects.
1. Updating the namespaces or projects.
1. Deleting namespaces/projects.
```mermaid
graph TD
subgraph "CI database (tables)"
E[other CI tables]
F{queries with joins allowed}
G[ci_project_mirrors]
H[ci_namespace_mirrors]
E---F
F---G
F---H
end
A---B
B---C
B---D
L["⛔ ← Joins are not allowed → ⛔"]
subgraph "Main database (tables)"
A[other main tables]
B{queries with joins allowed}
C[projects]
D[namespaces]
end
```
### Create and update
Syncing the data of newly created or updated namespaces or projects happens in this
order:
1. **On the `main` database**: Any `INSERT` or `UPDATE` on the `namespaces` or `projects` tables
adds an entry to the tables `namespaces_sync_events`, and `projects_sync_events`. These tables
also exist on the `main` database. These entries are added by triggers on both of the tables.
1. **On the model level**: After a commit happens on either of the source models `Namespace` or
`Project`, it schedules the corresponding Sidekiq jobs `Namespaces::ProcessSyncEventsWorker`
or `Projects::ProcessSyncEventsWorker` to run.
1. These workers then:
1. Read the entries from the tables `(namespaces/project)_sync_events`
from the `main` database, to check which namespaces or projects to sync.
1. Copy the data for any updated records into the target
tables `ci_namespace_mirrors`, `ci_project_mirrors`.
### Delete
When any of `namespaces` or `projects` are deleted, the target records on the mirrored
CI tables are deleted using the [loose foreign keys](loose_foreign_keys.md) (LFK) mechanism.
By having these items in the `config/gitlab_loose_foreign_keys.yml`, the LFK mechanism
was already working as expected. It deleted any records on the CI mirrored
tables that mapped to deleted `namespaces` or `projects` in the `main` database.
```yaml
ci_namespace_mirrors:
- table: namespaces
column: namespace_id
on_delete: async_delete
ci_project_mirrors:
- table: projects
column: project_id
on_delete: async_delete
```
## Consistency Checking
To make sure that both syncing mechanisms work as expected, we deploy
two extra worker jobs, triggered by cron jobs every few minutes:
1. `Database::CiNamespaceMirrorsConsistencyCheckWorker`
1. `Database::CiProjectMirrorsConsistencyCheckWorker`
These jobs:
1. Scan both of the source tables on the `main` database, using a cursor.
1. Compare the items in the `namespaces` and `projects` with the target tables on the `ci` database.
1. Report the items that are not in sync to Kibana and Prometheus.
1. Corrects any discrepancies.

View File

@ -250,7 +250,8 @@ You can use Vale:
Vale returns three types of results:
- **Error** - For branding and trademark issues, and words or phrases with ambiguous meanings.
- **Error** - For branding and trademark issues, words or phrases with ambiguous meanings, and anything that causes content on
the docs site to render incorrectly.
- **Warning** - For Technical Writing team style preferences.
- **Suggestion** - For basic technical writing tenets and best practices.

View File

@ -7,7 +7,8 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Container Scanning **(FREE)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/3672) in GitLab 10.4.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/3672) in GitLab 10.4.
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/86783) to Free tier in GitLab 15.0.
Your application's Docker image may itself be based on Docker images that contain known
vulnerabilities. By including an extra Container Scanning job in your pipeline that scans for those

View File

@ -113,6 +113,11 @@ GitLab supports these authentication methods:
- [SSH authentication](#ssh-authentication).
- Password.
When using password authentication, ensure you specify the username.
For a [project access token](../../settings/project_access_tokens.md) or
[group access token](../../../group/settings/group_access_tokens.md),
use the username (not token name) and the token as the password.
### SSH authentication
SSH authentication is mutual:

View File

@ -1,31 +0,0 @@
# frozen_string_literal: true
module Gitlab
module BackgroundMigration
# A job to update semver column in ci_runners in batches based on existing version values
class BackfillCiRunnerSemver < Gitlab::BackgroundMigration::BatchedMigrationJob
def perform
each_sub_batch(
operation_name: :backfill_ci_runner_semver,
batching_scope: ->(relation) { relation.where('semver::cidr IS NULL') }
) do |sub_batch|
ranged_query = sub_batch.select(
%q(id AS r_id,
substring(ci_runners.version FROM 'v?(\d+\.\d+\.\d+)') AS extracted_semver)
)
update_sql = <<~SQL
UPDATE
ci_runners
SET semver = extracted_semver
FROM (#{ranged_query.to_sql}) v
WHERE id = v.r_id
AND v.extracted_semver IS NOT NULL
SQL
connection.execute(update_sql)
end
end
end
end
end

View File

@ -11,7 +11,8 @@
variables:
# Setting this variable will affect all Security templates
# (SAST, Dependency Scanning, ...)
SECURE_ANALYZERS_PREFIX: "registry.gitlab.com/security-products"
TEMPLATE_REGISTRY_HOST: 'registry.gitlab.com'
SECURE_ANALYZERS_PREFIX: "$TEMPLATE_REGISTRY_HOST/security-products"
DS_EXCLUDED_ANALYZERS: ""
DS_EXCLUDED_PATHS: "spec, test, tests, tmp"
DS_MAJOR_VERSION: 3

View File

@ -11,7 +11,8 @@
variables:
# Setting this variable will affect all Security templates
# (SAST, Dependency Scanning, ...)
SECURE_ANALYZERS_PREFIX: "registry.gitlab.com/security-products"
TEMPLATE_REGISTRY_HOST: 'registry.gitlab.com'
SECURE_ANALYZERS_PREFIX: "$TEMPLATE_REGISTRY_HOST/security-products"
LICENSE_MANAGEMENT_SETUP_CMD: '' # If needed, specify a command to setup your environment with a custom package manager.
LICENSE_MANAGEMENT_VERSION: 4

View File

@ -6,7 +6,8 @@
variables:
# Setting this variable will affect all Security templates
# (SAST, Dependency Scanning, ...)
SECURE_ANALYZERS_PREFIX: "registry.gitlab.com/security-products"
TEMPLATE_REGISTRY_HOST: 'registry.gitlab.com'
SECURE_ANALYZERS_PREFIX: "$TEMPLATE_REGISTRY_HOST/security-products"
SAST_IMAGE_SUFFIX: ""
SAST_EXCLUDED_PATHS: "spec, test, tests, tmp"

View File

@ -6,7 +6,8 @@
variables:
# Setting this variable will affect all Security templates
# (SAST, Dependency Scanning, ...)
SECURE_ANALYZERS_PREFIX: "registry.gitlab.com/security-products"
TEMPLATE_REGISTRY_HOST: 'registry.gitlab.com'
SECURE_ANALYZERS_PREFIX: "$TEMPLATE_REGISTRY_HOST/security-products"
SAST_IMAGE_SUFFIX: ""
SAST_EXCLUDED_PATHS: "spec, test, tests, tmp"

View File

@ -6,7 +6,8 @@
variables:
# Setting this variable will affect all Security templates
# (SAST, Dependency Scanning, ...)
SECURE_ANALYZERS_PREFIX: "registry.gitlab.com/security-products"
TEMPLATE_REGISTRY_HOST: 'registry.gitlab.com'
SECURE_ANALYZERS_PREFIX: "$TEMPLATE_REGISTRY_HOST/security-products"
SAST_IMAGE_SUFFIX: ""
SAST_EXCLUDED_ANALYZERS: ""

View File

@ -6,7 +6,8 @@
variables:
# Setting this variable will affect all Security templates
# (SAST, Dependency Scanning, ...)
SECURE_ANALYZERS_PREFIX: "registry.gitlab.com/security-products"
TEMPLATE_REGISTRY_HOST: 'registry.gitlab.com'
SECURE_ANALYZERS_PREFIX: "$TEMPLATE_REGISTRY_HOST/security-products"
SAST_IMAGE_SUFFIX: ""
SAST_EXCLUDED_ANALYZERS: ""

View File

@ -5,7 +5,8 @@
# How to set: https://docs.gitlab.com/ee/ci/yaml/#variables
variables:
SECURE_ANALYZERS_PREFIX: "registry.gitlab.com/security-products"
TEMPLATE_REGISTRY_HOST: 'registry.gitlab.com'
SECURE_ANALYZERS_PREFIX: "$TEMPLATE_REGISTRY_HOST/security-products"
SECRET_DETECTION_IMAGE_SUFFIX: ""
SECRETS_ANALYZER_VERSION: "4"

View File

@ -5,7 +5,8 @@
# How to set: https://docs.gitlab.com/ee/ci/yaml/#variables
variables:
SECURE_ANALYZERS_PREFIX: "registry.gitlab.com/security-products"
TEMPLATE_REGISTRY_HOST: 'registry.gitlab.com'
SECURE_ANALYZERS_PREFIX: "$TEMPLATE_REGISTRY_HOST/security-products"
SECRET_DETECTION_IMAGE_SUFFIX: ""
SECRETS_ANALYZER_VERSION: "4"
SECRET_DETECTION_EXCLUDED_PATHS: ""

View File

@ -24,7 +24,8 @@
variables:
# Setting this variable affects all Security templates
# (SAST, Dependency Scanning, ...)
SECURE_ANALYZERS_PREFIX: "registry.gitlab.com/security-products"
TEMPLATE_REGISTRY_HOST: 'registry.gitlab.com'
SECURE_ANALYZERS_PREFIX: "$TEMPLATE_REGISTRY_HOST/security-products"
#
FUZZAPI_VERSION: "2"
FUZZAPI_IMAGE_SUFFIX: ""

View File

@ -24,7 +24,8 @@
variables:
# Setting this variable affects all Security templates
# (SAST, Dependency Scanning, ...)
SECURE_ANALYZERS_PREFIX: "registry.gitlab.com/security-products"
TEMPLATE_REGISTRY_HOST: 'registry.gitlab.com'
SECURE_ANALYZERS_PREFIX: "$TEMPLATE_REGISTRY_HOST/security-products"
#
FUZZAPI_VERSION: "2"
FUZZAPI_IMAGE_SUFFIX: ""

View File

@ -24,7 +24,8 @@
variables:
# Setting this variable affects all Security templates
# (SAST, Dependency Scanning, ...)
SECURE_ANALYZERS_PREFIX: "registry.gitlab.com/security-products"
TEMPLATE_REGISTRY_HOST: 'registry.gitlab.com'
SECURE_ANALYZERS_PREFIX: "$TEMPLATE_REGISTRY_HOST/security-products"
#
DAST_API_VERSION: "2"
DAST_API_IMAGE_SUFFIX: ""

View File

@ -24,7 +24,8 @@
variables:
# Setting this variable affects all Security templates
# (SAST, Dependency Scanning, ...)
SECURE_ANALYZERS_PREFIX: "registry.gitlab.com/security-products"
TEMPLATE_REGISTRY_HOST: 'registry.gitlab.com'
SECURE_ANALYZERS_PREFIX: "$TEMPLATE_REGISTRY_HOST/security-products"
#
DAST_API_VERSION: "2"
DAST_API_IMAGE_SUFFIX: ""

View File

@ -10,7 +10,8 @@ stages:
- dast
variables:
SECURE_ANALYZERS_PREFIX: "registry.gitlab.com/security-products"
TEMPLATE_REGISTRY_HOST: 'registry.gitlab.com'
SECURE_ANALYZERS_PREFIX: "$TEMPLATE_REGISTRY_HOST/security-products"
DAST_API_VERSION: "2"
DAST_API_IMAGE_SUFFIX: ""
DAST_API_IMAGE: api-security

View File

@ -13,7 +13,8 @@ variables:
DAST_VERSION: 3
# Setting this variable will affect all Security templates
# (SAST, Dependency Scanning, ...)
SECURE_ANALYZERS_PREFIX: "registry.gitlab.com/security-products"
TEMPLATE_REGISTRY_HOST: 'registry.gitlab.com'
SECURE_ANALYZERS_PREFIX: "$TEMPLATE_REGISTRY_HOST/security-products"
dast:
stage: dast

View File

@ -25,7 +25,8 @@ variables:
DAST_VERSION: 3
# Setting this variable will affect all Security templates
# (SAST, Dependency Scanning, ...)
SECURE_ANALYZERS_PREFIX: "registry.gitlab.com/security-products"
TEMPLATE_REGISTRY_HOST: 'registry.gitlab.com'
SECURE_ANALYZERS_PREFIX: "$TEMPLATE_REGISTRY_HOST/security-products"
dast:
stage: dast

View File

@ -25,7 +25,8 @@ variables:
DAST_VERSION: 3
# Setting this variable will affect all Security templates
# (SAST, Dependency Scanning, ...)
SECURE_ANALYZERS_PREFIX: "registry.gitlab.com/security-products"
TEMPLATE_REGISTRY_HOST: 'registry.gitlab.com'
SECURE_ANALYZERS_PREFIX: "$TEMPLATE_REGISTRY_HOST/security-products"
dast:
stage: dast

View File

@ -16,7 +16,8 @@
variables:
# Setting this variable will affect all Security templates
# (SAST, Dependency Scanning, ...)
SECURE_ANALYZERS_PREFIX: "registry.gitlab.com/security-products"
TEMPLATE_REGISTRY_HOST: 'registry.gitlab.com'
SECURE_ANALYZERS_PREFIX: "$TEMPLATE_REGISTRY_HOST/security-products"
SECURE_BINARIES_ANALYZERS: >-
bandit, brakeman, gosec, spotbugs, flawfinder, phpcs-security-audit, security-code-scan, nodejs-scan, eslint, secrets, sobelow, pmd-apex, kics, kubesec, semgrep, gemnasium, gemnasium-maven, gemnasium-python,
license-finder,

View File

@ -0,0 +1,32 @@
# frozen_string_literal: true
module Gitlab
module GithubImport
module Importer
module Events
# Base class for importing issue events during project import from GitHub
class BaseImporter
# project - An instance of `Project`.
# user_finder - An instance of `Gitlab::GithubImport::UserFinder`.
def initialize(project, user_finder)
@project = project
@user_finder = user_finder
end
# issue_event - An instance of `Gitlab::GithubImport::Representation::IssueEvent`.
def execute(issue_event)
raise NotImplementedError
end
private
attr_reader :project, :user_finder
def author_id(issue_event, author_key: :actor)
user_finder.author_id_for(issue_event, author_key: author_key).first
end
end
end
end
end
end

View File

@ -0,0 +1,52 @@
# frozen_string_literal: true
module Gitlab
module GithubImport
module Importer
module Events
class ChangedAssignee < BaseImporter
def execute(issue_event)
assignee_id = author_id(issue_event, author_key: :assignee)
assigner_id = author_id(issue_event, author_key: :assigner)
note_body = parse_body(issue_event, assigner_id, assignee_id)
create_note(issue_event, note_body, assigner_id)
end
private
def create_note(issue_event, note_body, assigner_id)
Note.create!(
system: true,
noteable_type: Issue.name,
noteable_id: issue_event.issue_db_id,
project: project,
author_id: assigner_id,
note: note_body,
system_note_metadata: SystemNoteMetadata.new(
{
action: "assignee",
created_at: issue_event.created_at,
updated_at: issue_event.created_at
}
),
created_at: issue_event.created_at,
updated_at: issue_event.created_at
)
end
def parse_body(issue_event, assigner_id, assignee_id)
Gitlab::I18n.with_default_locale do
if issue_event.event == "unassigned"
"unassigned #{User.find(assigner_id).to_reference}"
else
"assigned to #{User.find(assignee_id).to_reference}"
end
end
end
end
end
end
end
end

View File

@ -4,25 +4,17 @@ module Gitlab
module GithubImport
module Importer
module Events
class ChangedLabel
def initialize(project, user_id)
@project = project
@user_id = user_id
end
# issue_event - An instance of `Gitlab::GithubImport::Representation::IssueEvent`.
class ChangedLabel < BaseImporter
def execute(issue_event)
create_event(issue_event)
end
private
attr_reader :project, :user_id
def create_event(issue_event)
ResourceLabelEvent.create!(
issue_id: issue_event.issue_db_id,
user_id: user_id,
user_id: author_id(issue_event),
label_id: label_finder.id_for(issue_event.label_title),
action: action(issue_event.event),
created_at: issue_event.created_at

View File

@ -4,20 +4,12 @@ module Gitlab
module GithubImport
module Importer
module Events
class ChangedMilestone
attr_reader :project, :user_id
class ChangedMilestone < BaseImporter
# GitHub API doesn't provide the historical state of an issue for
# de/milestoned issue events. So we'll assign the default state to
# those events that are imported from GitHub.
DEFAULT_STATE = Issue.available_states[:opened]
def initialize(project, user_id)
@project = project
@user_id = user_id
end
# issue_event - An instance of `Gitlab::GithubImport::Representation::IssueEvent`.
def execute(issue_event)
create_event(issue_event)
end
@ -27,7 +19,7 @@ module Gitlab
def create_event(issue_event)
ResourceMilestoneEvent.create!(
issue_id: issue_event.issue_db_id,
user_id: user_id,
user_id: author_id(issue_event),
created_at: issue_event.created_at,
milestone_id: project.milestones.find_by_title(issue_event.milestone_title)&.id,
action: action(issue_event.event),

View File

@ -4,15 +4,7 @@ module Gitlab
module GithubImport
module Importer
module Events
class Closed
attr_reader :project, :user_id
def initialize(project, user_id)
@project = project
@user_id = user_id
end
# issue_event - An instance of `Gitlab::GithubImport::Representation::IssueEvent`.
class Closed < BaseImporter
def execute(issue_event)
create_event(issue_event)
create_state_event(issue_event)
@ -23,7 +15,7 @@ module Gitlab
def create_event(issue_event)
Event.create!(
project_id: project.id,
author_id: user_id,
author_id: author_id(issue_event),
action: 'closed',
target_type: Issue.name,
target_id: issue_event.issue_db_id,
@ -34,7 +26,7 @@ module Gitlab
def create_state_event(issue_event)
ResourceStateEvent.create!(
user_id: user_id,
user_id: author_id(issue_event),
issue_id: issue_event.issue_db_id,
source_commit: issue_event.commit_id,
state: 'closed',

View File

@ -4,15 +4,7 @@ module Gitlab
module GithubImport
module Importer
module Events
class CrossReferenced
attr_reader :project, :user_id
def initialize(project, user_id)
@project = project
@user_id = user_id
end
# issue_event - An instance of `Gitlab::GithubImport::Representation::IssueEvent`.
class CrossReferenced < BaseImporter
def execute(issue_event)
mentioned_in_record_class = mentioned_in_type(issue_event)
mentioned_in_number = issue_event.source.dig(:issue, :number)
@ -21,14 +13,15 @@ module Gitlab
)
return if mentioned_in_record.nil?
user_id = author_id(issue_event)
note_body = cross_reference_note_content(mentioned_in_record.gfm_reference(project))
track_activity(mentioned_in_record_class)
create_note(issue_event, note_body)
track_activity(mentioned_in_record_class, user_id)
create_note(issue_event, note_body, user_id)
end
private
def track_activity(mentioned_in_class)
def track_activity(mentioned_in_class, user_id)
return if mentioned_in_class != Issue
Gitlab::UsageDataCounters::HLLRedisCounter.track_event(
@ -37,7 +30,7 @@ module Gitlab
)
end
def create_note(issue_event, note_body)
def create_note(issue_event, note_body, user_id)
Note.create!(
system: true,
noteable_type: Issue.name,

View File

@ -4,27 +4,19 @@ module Gitlab
module GithubImport
module Importer
module Events
class Renamed
def initialize(project, user_id)
@project = project
@user_id = user_id
end
# issue_event - An instance of `Gitlab::GithubImport::Representation::IssueEvent`
class Renamed < BaseImporter
def execute(issue_event)
Note.create!(note_params(issue_event))
end
private
attr_reader :project, :user_id
def note_params(issue_event)
{
noteable_id: issue_event.issue_db_id,
noteable_type: Issue.name,
project_id: project.id,
author_id: user_id,
author_id: author_id(issue_event),
note: parse_body(issue_event),
system: true,
created_at: issue_event.created_at,

View File

@ -4,15 +4,7 @@ module Gitlab
module GithubImport
module Importer
module Events
class Reopened
attr_reader :project, :user_id
def initialize(project, user_id)
@project = project
@user_id = user_id
end
# issue_event - An instance of `Gitlab::GithubImport::Representation::IssueEvent`.
class Reopened < BaseImporter
def execute(issue_event)
create_event(issue_event)
create_state_event(issue_event)
@ -23,7 +15,7 @@ module Gitlab
def create_event(issue_event)
Event.create!(
project_id: project.id,
author_id: user_id,
author_id: author_id(issue_event),
action: 'reopened',
target_type: Issue.name,
target_id: issue_event.issue_db_id,
@ -34,7 +26,7 @@ module Gitlab
def create_state_event(issue_event)
ResourceStateEvent.create!(
user_id: user_id,
user_id: author_id(issue_event),
issue_id: issue_event.issue_db_id,
state: 'reopened',
created_at: issue_event.created_at

View File

@ -17,25 +17,25 @@ module Gitlab
end
def execute
case issue_event.event
when 'closed'
Gitlab::GithubImport::Importer::Events::Closed.new(project, author_id)
.execute(issue_event)
when 'reopened'
Gitlab::GithubImport::Importer::Events::Reopened.new(project, author_id)
.execute(issue_event)
when 'labeled', 'unlabeled'
Gitlab::GithubImport::Importer::Events::ChangedLabel.new(project, author_id)
.execute(issue_event)
when 'renamed'
Gitlab::GithubImport::Importer::Events::Renamed.new(project, author_id)
.execute(issue_event)
when 'milestoned', 'demilestoned'
Gitlab::GithubImport::Importer::Events::ChangedMilestone.new(project, author_id)
.execute(issue_event)
when 'cross-referenced'
Gitlab::GithubImport::Importer::Events::CrossReferenced.new(project, author_id)
.execute(issue_event)
event_importer = case issue_event.event
when 'closed'
Gitlab::GithubImport::Importer::Events::Closed
when 'reopened'
Gitlab::GithubImport::Importer::Events::Reopened
when 'labeled', 'unlabeled'
Gitlab::GithubImport::Importer::Events::ChangedLabel
when 'renamed'
Gitlab::GithubImport::Importer::Events::Renamed
when 'milestoned', 'demilestoned'
Gitlab::GithubImport::Importer::Events::ChangedMilestone
when 'cross-referenced'
Gitlab::GithubImport::Importer::Events::CrossReferenced
when 'assigned', 'unassigned'
Gitlab::GithubImport::Importer::Events::ChangedAssignee
end
if event_importer
event_importer.new(project, user_finder).execute(issue_event)
else
Gitlab::GithubImport::Logger.debug(
message: 'UNSUPPORTED_EVENT_TYPE',
@ -43,13 +43,6 @@ module Gitlab
)
end
end
private
def author_id
id, _status = user_finder.author_id_for(issue_event, author_key: :actor)
id
end
end
end
end

View File

@ -10,36 +10,9 @@ module Gitlab
attr_reader :attributes
expose_attribute :id, :actor, :event, :commit_id, :label_title, :old_title, :new_title,
:milestone_title, :source, :created_at
:milestone_title, :source, :assignee, :assigner, :created_at
expose_attribute :issue_db_id # set in SingleEndpointIssueEventsImporter#each_associated
# Builds a event from a GitHub API response.
#
# event - An instance of `Sawyer::Resource` containing the event details.
def self.from_api_response(event)
new(
id: event.id,
actor: event.actor && Representation::User.from_api_response(event.actor),
event: event.event,
commit_id: event.commit_id,
label_title: event.label && event.label[:name],
old_title: event.rename && event.rename[:from],
new_title: event.rename && event.rename[:to],
source: event.source,
issue_db_id: event.issue_db_id,
milestone_title: event.milestone && event.milestone[:title],
created_at: event.created_at
)
end
# Builds a event using a Hash that was built from a JSON payload.
def self.from_json_hash(raw_hash)
hash = Representation.symbolize_hash(raw_hash)
hash[:actor] &&= Representation::User.from_json_hash(hash[:actor])
new(hash)
end
# attributes - A Hash containing the event details. The keys of this
# Hash (and any nested hashes) must be symbols.
def initialize(attributes)
@ -49,6 +22,52 @@ module Gitlab
def github_identifiers
{ id: id }
end
class << self
# Builds an event from a GitHub API response.
#
# event - An instance of `Sawyer::Resource` containing the event details.
def from_api_response(event)
new(
id: event.id,
actor: user_representation(event.actor),
event: event.event,
commit_id: event.commit_id,
label_title: event.label && event.label[:name],
old_title: event.rename && event.rename[:from],
new_title: event.rename && event.rename[:to],
milestone_title: event.milestone && event.milestone[:title],
source: event.source,
assignee: user_representation(event.assignee),
assigner: user_representation(event.assigner),
issue_db_id: event.issue_db_id,
created_at: event.created_at
)
end
# Builds an event using a Hash that was built from a JSON payload.
def from_json_hash(raw_hash)
hash = Representation.symbolize_hash(raw_hash)
hash[:actor] = user_representation(hash[:actor], source: :hash)
hash[:assignee] = user_representation(hash[:assignee], source: :hash)
hash[:assigner] = user_representation(hash[:assigner], source: :hash)
new(hash)
end
private
def user_representation(data, source: :api_response)
return unless data
case source
when :api_response
Representation::User.from_api_response(data)
when :hash
Representation::User.from_json_hash(data)
end
end
end
end
end
end

View File

@ -40,7 +40,17 @@ module Gitlab
# If the object has no author ID we'll use the ID of the GitLab ghost
# user.
def author_id_for(object, author_key: :author)
user_info = author_key == :actor ? object&.actor : object&.author
user_info = case author_key
when :actor
object&.actor
when :assignee
object&.assignee
when :assigner
object&.assigner
else
object&.author
end
id = user_info ? user_id_for(user_info) : GithubImport.ghost_user_id
if id

View File

@ -0,0 +1,57 @@
# frozen_string_literal: true
module Gitlab
module ImportExport
module Project
class RelationSaver
def initialize(project:, shared:, relation:)
@project = project
@relation = relation
@shared = shared
end
def save
if root_relation?
serializer.serialize_root
else
serializer.serialize_relation(relation_schema)
end
true
rescue StandardError => e
shared.error(e)
false
end
private
attr_reader :project, :relation, :shared
def serializer
@serializer ||= ::Gitlab::ImportExport::Json::StreamingSerializer.new(
project,
reader.project_tree,
json_writer,
exportable_path: 'project'
)
end
def root_relation?
relation == Projects::ImportExport::RelationExport::ROOT_RELATION
end
def relation_schema
reader.project_tree[:include].find { |include| include[relation.to_sym] }
end
def reader
@reader ||= ::Gitlab::ImportExport::Reader.new(shared: shared)
end
def json_writer
@json_writer ||= ::Gitlab::ImportExport::Json::NdjsonWriter.new(shared.export_path)
end
end
end
end
end

View File

@ -998,9 +998,6 @@ msgstr[1] ""
msgid "%{strongOpen}Warning:%{strongClose} SAML group links can cause GitLab to automatically remove members from groups."
msgstr ""
msgid "%{strongStart}Need your attention%{strongEnd} are the merge requests that need your help to move forward, as an assignee or reviewer."
msgstr ""
msgid "%{strongStart}Tip:%{strongEnd} You can also check out merge requests locally. %{linkStart}Learn more.%{linkEnd}"
msgstr ""
@ -2189,9 +2186,6 @@ msgstr ""
msgid "Add approvers"
msgstr ""
msgid "Add attention request"
msgstr ""
msgid "Add broadcast message"
msgstr ""
@ -4857,9 +4851,6 @@ msgstr ""
msgid "Approved-By"
msgstr ""
msgid "Approved. Your attention request was removed."
msgstr ""
msgid "Approver"
msgstr ""
@ -5201,9 +5192,6 @@ msgstr ""
msgid "Assigned to you"
msgstr ""
msgid "Assigned user(s). Your attention request was removed."
msgstr ""
msgid "Assignee"
msgid_plural "%d Assignees"
msgstr[0] ""
@ -5259,12 +5247,6 @@ msgstr[1] ""
msgid "Attaching the file failed."
msgstr ""
msgid "Attention"
msgstr ""
msgid "Attention requested"
msgstr ""
msgid "Audit Events"
msgstr ""
@ -25492,12 +25474,6 @@ msgstr ""
msgid "MrList|Assigned to %{name}"
msgstr ""
msgid "MrList|Attention requested from assignee %{name}"
msgstr ""
msgid "MrList|Attention requested from reviewer %{name}"
msgstr ""
msgid "MrList|Review requested from %{name}"
msgstr ""
@ -25707,9 +25683,6 @@ msgstr ""
msgid "Need help?"
msgstr ""
msgid "Need your attention"
msgstr ""
msgid "Needs"
msgstr ""
@ -26012,9 +25985,6 @@ msgstr ""
msgid "No assignee"
msgstr ""
msgid "No attention request"
msgstr ""
msgid "No authentication methods configured."
msgstr ""
@ -32438,9 +32408,6 @@ msgstr ""
msgid "Removed attention from %{users_sentence}."
msgstr ""
msgid "Removed attention request from @%{username}"
msgstr ""
msgid "Removed group can not be restored!"
msgstr ""
@ -32998,21 +32965,12 @@ msgstr ""
msgid "Requested attention from %{users_sentence}."
msgstr ""
msgid "Requested attention from @%{username}"
msgstr ""
msgid "Requested attention from @%{username}. Your own attention request was removed."
msgstr ""
msgid "Requested design version does not exist."
msgstr ""
msgid "Requested review"
msgstr ""
msgid "Requested review. Your attention request was removed."
msgstr ""
msgid "Requested states are invalid"
msgstr ""
@ -36480,9 +36438,6 @@ msgstr ""
msgid "Solution"
msgstr ""
msgid "Some actions remove attention requests, like a reviewer approving or anyone merging the merge request."
msgstr ""
msgid "Some changes are not shown"
msgstr ""
@ -40461,9 +40416,6 @@ msgstr ""
msgid "To add the entry manually, provide the following details to the application on your phone."
msgstr ""
msgid "To ask someone to look at a merge request, select %{strongStart}Request attention%{strongEnd}. Select again to remove the request."
msgstr ""
msgid "To complete registration, we need additional details from you."
msgstr ""
@ -41586,9 +41538,6 @@ msgstr ""
msgid "Updating"
msgstr ""
msgid "Updating the attention request for %{username} failed."
msgstr ""
msgid "Updating…"
msgstr ""

View File

@ -245,9 +245,9 @@ RSpec.describe Gitlab::SidekiqCluster::CLI, stub_settings_source: true do # rubo
it 'expands multiple queue groups correctly' do
expected_workers =
if Gitlab.ee?
[%w[chat_notification], %w[project_export project_template_export]]
[%w[chat_notification], %w[project_export projects_import_export_relation_export project_template_export]]
else
[%w[chat_notification], %w[project_export]]
[%w[chat_notification], %w[project_export projects_import_export_relation_export]]
end
expect(Gitlab::SidekiqCluster)

View File

@ -9,8 +9,6 @@ RSpec.describe 'Merge requests > User mass updates', :js do
let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
before do
stub_feature_flags(mr_attention_requests: false)
project.add_maintainer(user)
project.add_maintainer(user2)
sign_in(user)
@ -63,18 +61,6 @@ RSpec.describe 'Merge requests > User mass updates', :js do
expect(find('.merge-request')).to have_link "Assigned to #{user.name}"
end
describe 'with attention requests feature flag on' do
before do
stub_feature_flags(mr_attention_requests: true)
end
it 'updates merge request with assignee' do
change_assignee(user2.name)
expect(find('.issuable-meta a.author-link')[:title]).to eq "Attention requested from assignee #{user2.name}"
end
end
end
describe 'remove assignee' do

View File

@ -1,88 +0,0 @@
import { shallowMount } from '@vue/test-utils';
import { GlPopover, GlButton, GlSprintf, GlIcon } from '@gitlab/ui';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import NavigationPopover from '~/attention_requests/components/navigation_popover.vue';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { makeMockUserCalloutDismisser } from 'helpers/mock_user_callout_dismisser';
let wrapper;
let dismiss;
function createComponent(provideData = {}, shouldShowCallout = true) {
wrapper = shallowMount(NavigationPopover, {
provide: {
message: ['Test'],
observerElSelector: '.js-test',
observerElToggledClass: 'show',
featureName: 'attention_requests',
popoverTarget: '.js-test-popover',
...provideData,
},
stubs: {
UserCalloutDismisser: makeMockUserCalloutDismisser({
dismiss,
shouldShowCallout,
}),
GlSprintf,
},
});
}
describe('Attention requests navigation popover', () => {
beforeEach(() => {
setHTMLFixture('<div><div class="js-test-popover"></div><div class="js-test"></div></div>');
dismiss = jest.fn();
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
resetHTMLFixture();
});
it('hides popover if callout is disabled', () => {
createComponent({}, false);
expect(wrapper.findComponent(GlPopover).exists()).toBe(false);
});
it('shows popover if callout is enabled', () => {
createComponent();
expect(wrapper.findComponent(GlPopover).exists()).toBe(true);
});
it.each`
isDesktop | device | expectedPlacement
${true} | ${'desktop'} | ${'left'}
${false} | ${'mobile'} | ${'bottom'}
`(
'sets popover position to $expectedPlacement on $device',
({ isDesktop, expectedPlacement }) => {
jest.spyOn(bp, 'isDesktop').mockReturnValue(isDesktop);
createComponent();
expect(wrapper.findComponent(GlPopover).props('placement')).toBe(expectedPlacement);
},
);
it('calls dismiss when clicking action button', () => {
createComponent();
wrapper
.findComponent(GlButton)
.vm.$emit('click', { preventDefault() {}, stopPropagation() {} });
expect(dismiss).toHaveBeenCalled();
});
it('shows icon in text', () => {
createComponent({ showAttentionIcon: true, message: ['%{strongStart}Test%{strongEnd}'] });
const icon = wrapper.findComponent(GlIcon);
expect(icon.exists()).toBe(true);
expect(icon.props('name')).toBe('attention');
});
});

View File

@ -1,5 +1,5 @@
import { GlAlert, GlBadge, GlLoadingIcon, GlTabs } from '@gitlab/ui';
import { createLocalVue, mount, shallowMount } from '@vue/test-utils';
import { mount, shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import Vue, { nextTick } from 'vue';
import createMockApollo from 'helpers/mock_apollo_helper';
@ -30,8 +30,7 @@ import {
mockLintResponseWithoutMerged,
} from '../mock_data';
const localVue = createLocalVue();
localVue.use(VueApollo);
Vue.use(VueApollo);
Vue.config.ignoredElements = ['gl-emoji'];
@ -88,7 +87,6 @@ describe('Pipeline editor tabs component', () => {
provide,
mountFn,
options: {
localVue,
apolloProvider: mockApollo,
},
});

View File

@ -1,5 +1,5 @@
import { GlToast } from '@gitlab/ui';
import { createLocalVue } from '@vue/test-utils';
import Vue from 'vue';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import RegistrationToken from '~/runner/components/registration/registration_token.vue';
import InputCopyToggleVisibility from '~/vue_shared/components/form/input_copy_toggle_visibility.vue';
@ -11,28 +11,17 @@ describe('RegistrationToken', () => {
let wrapper;
let showToast;
Vue.use(GlToast);
const findInputCopyToggleVisibility = () => wrapper.findComponent(InputCopyToggleVisibility);
const vueWithGlToast = () => {
const localVue = createLocalVue();
localVue.use(GlToast);
return localVue;
};
const createComponent = ({
props = {},
withGlToast = true,
mountFn = shallowMountExtended,
} = {}) => {
const localVue = withGlToast ? vueWithGlToast() : undefined;
const createComponent = ({ props = {}, mountFn = shallowMountExtended } = {}) => {
wrapper = mountFn(RegistrationToken, {
propsData: {
value: mockToken,
inputId: 'token-value',
...props,
},
localVue,
});
showToast = wrapper.vm.$toast ? jest.spyOn(wrapper.vm.$toast, 'show') : null;
@ -69,13 +58,5 @@ describe('RegistrationToken', () => {
expect(showToast).toHaveBeenCalledTimes(1);
expect(showToast).toHaveBeenCalledWith('Registration token copied!');
});
it('does not fail when toast is not defined', () => {
createComponent({ withGlToast: false });
findInputCopyToggleVisibility().vm.$emit('copy');
// This block also tests for unhandled errors
expect(showToast).toBeNull();
});
});
});

View File

@ -1,121 +0,0 @@
import { GlButton } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import AttentionRequestedToggle from '~/sidebar/components/attention_requested_toggle.vue';
let wrapper;
function factory(propsData = {}) {
wrapper = mount(AttentionRequestedToggle, { propsData });
}
const findToggle = () => wrapper.findComponent(GlButton);
describe('Attention require toggle', () => {
afterEach(() => {
wrapper.destroy();
});
it('renders button', () => {
factory({
type: 'reviewer',
user: { attention_requested: false, can_update_merge_request: true },
});
expect(findToggle().exists()).toBe(true);
});
it.each`
attentionRequested | icon
${true} | ${'attention-solid'}
${false} | ${'attention'}
`(
'renders $icon icon when attention_requested is $attentionRequested',
({ attentionRequested, icon }) => {
factory({
type: 'reviewer',
user: { attention_requested: attentionRequested, can_update_merge_request: true },
});
expect(findToggle().props('icon')).toBe(icon);
},
);
it.each`
attentionRequested | selected
${true} | ${true}
${false} | ${false}
`(
'renders button with as selected when $selected when attention_requested is $attentionRequested',
({ attentionRequested, selected }) => {
factory({
type: 'reviewer',
user: { attention_requested: attentionRequested, can_update_merge_request: true },
});
expect(findToggle().props('selected')).toBe(selected);
},
);
it('emits toggle-attention-requested on click', async () => {
factory({
type: 'reviewer',
user: { attention_requested: true, can_update_merge_request: true },
});
await findToggle().trigger('click');
expect(wrapper.emitted('toggle-attention-requested')[0]).toEqual([
{
user: { attention_requested: true, can_update_merge_request: true },
callback: expect.anything(),
direction: 'remove',
},
]);
});
it('does not emit toggle-attention-requested on click if can_update_merge_request is false', async () => {
factory({
type: 'reviewer',
user: { attention_requested: true, can_update_merge_request: false },
});
await findToggle().trigger('click');
expect(wrapper.emitted('toggle-attention-requested')).toBe(undefined);
});
it('sets loading on click', async () => {
factory({
type: 'reviewer',
user: { attention_requested: true, can_update_merge_request: true },
});
await findToggle().trigger('click');
expect(findToggle().props('loading')).toBe(true);
});
it.each`
type | attentionRequested | tooltip | canUpdateMergeRequest
${'reviewer'} | ${true} | ${AttentionRequestedToggle.i18n.removeAttentionRequest} | ${true}
${'reviewer'} | ${false} | ${AttentionRequestedToggle.i18n.addAttentionRequest} | ${true}
${'assignee'} | ${false} | ${AttentionRequestedToggle.i18n.addAttentionRequest} | ${true}
${'reviewer'} | ${true} | ${AttentionRequestedToggle.i18n.attentionRequestedNoPermission} | ${false}
${'reviewer'} | ${false} | ${AttentionRequestedToggle.i18n.noAttentionRequestedNoPermission} | ${false}
${'assignee'} | ${true} | ${AttentionRequestedToggle.i18n.attentionRequestedNoPermission} | ${false}
${'assignee'} | ${false} | ${AttentionRequestedToggle.i18n.noAttentionRequestedNoPermission} | ${false}
`(
'sets tooltip as $tooltip when attention_requested is $attentionRequested, type is $type and, can_update_merge_request is $canUpdateMergeRequest',
({ type, attentionRequested, tooltip, canUpdateMergeRequest }) => {
factory({
type,
user: {
attention_requested: attentionRequested,
can_update_merge_request: canUpdateMergeRequest,
},
});
expect(findToggle().attributes('aria-label')).toBe(tooltip);
},
);
});

View File

@ -1,6 +1,5 @@
import { shallowMount } from '@vue/test-utils';
import { TEST_HOST } from 'helpers/test_constants';
import AttentionRequestedToggle from '~/sidebar/components/attention_requested_toggle.vue';
import ReviewerAvatarLink from '~/sidebar/components/reviewers/reviewer_avatar_link.vue';
import UncollapsedReviewerList from '~/sidebar/components/reviewers/uncollapsed_reviewer_list.vue';
import userDataMock from '../../user_data_mock';
@ -119,18 +118,4 @@ describe('UncollapsedReviewerList component', () => {
expect(wrapper.find('[data-testid="re-request-success"]').exists()).toBe(true);
});
});
it('hides re-request review button when attentionRequired feature flag is enabled', () => {
createComponent({ users: [userDataMock()] }, { mrAttentionRequests: true });
expect(wrapper.findAll('[data-testid="re-request-button"]').length).toBe(0);
});
it('emits toggle-attention-requested', () => {
createComponent({ users: [userDataMock()] }, { mrAttentionRequests: true });
wrapper.find(AttentionRequestedToggle).vm.$emit('toggle-attention-requested', 'data');
expect(wrapper.emitted('toggle-attention-requested')[0]).toEqual(['data']);
});
});

View File

@ -1,12 +1,9 @@
import MockAdapter from 'axios-mock-adapter';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import * as urlUtility from '~/lib/utils/url_utility';
import SidebarService, { gqClient } from '~/sidebar/services/sidebar_service';
import SidebarMediator from '~/sidebar/sidebar_mediator';
import SidebarStore from '~/sidebar/stores/sidebar_store';
import toast from '~/vue_shared/plugins/global_toast';
import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
import Mock from './mock_data';
jest.mock('~/flash');
@ -122,93 +119,4 @@ describe('Sidebar mediator', () => {
urlSpy.mockRestore();
});
});
describe('toggleAttentionRequested', () => {
let requestAttentionMock;
let removeAttentionRequestMock;
beforeEach(() => {
requestAttentionMock = jest.spyOn(mediator.service, 'requestAttention').mockResolvedValue();
removeAttentionRequestMock = jest
.spyOn(mediator.service, 'removeAttentionRequest')
.mockResolvedValue();
});
it.each`
attentionIsCurrentlyRequested | serviceMethod
${true} | ${'remove'}
${false} | ${'add'}
`(
"calls the $serviceMethod service method when the user's attention request is set to $attentionIsCurrentlyRequested",
async ({ serviceMethod }) => {
const methods = {
add: requestAttentionMock,
remove: removeAttentionRequestMock,
};
mediator.store.reviewers = [{ id: 1, attention_requested: false, username: 'root' }];
await mediator.toggleAttentionRequested('reviewer', {
user: { id: 1, username: 'root' },
callback: jest.fn(),
direction: serviceMethod,
});
expect(methods[serviceMethod]).toHaveBeenCalledWith(1);
expect(refreshUserMergeRequestCounts).toHaveBeenCalled();
},
);
it.each`
type | method
${'reviewer'} | ${'findReviewer'}
`('finds $type', ({ type, method }) => {
const methodSpy = jest.spyOn(mediator.store, method);
mediator.toggleAttentionRequested(type, { user: { id: 1 }, callback: jest.fn() });
expect(methodSpy).toHaveBeenCalledWith({ id: 1 });
});
it.each`
attentionRequested | toastMessage
${true} | ${'Removed attention request from @root'}
${false} | ${'Requested attention from @root'}
`(
'it creates toast $toastMessage when attention_requested is $attentionRequested',
async ({ attentionRequested, toastMessage }) => {
mediator.store.reviewers = [
{ id: 1, attention_requested: attentionRequested, username: 'root' },
];
await mediator.toggleAttentionRequested('reviewer', {
user: { id: 1, username: 'root' },
callback: jest.fn(),
});
expect(toast).toHaveBeenCalledWith(toastMessage);
},
);
describe('errors', () => {
beforeEach(() => {
jest
.spyOn(mediator.service, 'removeAttentionRequest')
.mockRejectedValueOnce(new Error('Something went wrong'));
});
it('shows an error message', async () => {
await mediator.toggleAttentionRequested('reviewer', {
user: { id: 1, username: 'root' },
callback: jest.fn(),
direction: 'remove',
});
expect(createFlash).toHaveBeenCalledWith(
expect.objectContaining({
message: 'Updating the attention request for root failed.',
}),
);
});
});
});
});

View File

@ -1,5 +1,5 @@
import { createLocalVue, shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import { GlSprintf } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
import produce from 'immer';
@ -71,8 +71,8 @@ const createTestService = () => ({
merge: jest.fn(),
poll: jest.fn().mockResolvedValue(),
});
const localVue = createLocalVue();
localVue.use(VueApollo);
Vue.use(VueApollo);
let wrapper;
let readyToMergeResponseSpy;
@ -93,7 +93,6 @@ const createComponent = (
restructuredMrWidget = false,
) => {
wrapper = shallowMount(ReadyToMerge, {
localVue,
propsData: {
mr: createTestMr(customConfig),
service: createTestService(),

View File

@ -1,19 +1,17 @@
import { nextTick } from 'vue';
import { createLocalVue, mount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import { mount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import { GlBanner } from '@gitlab/ui';
import App from '~/work_items_hierarchy/components/app.vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
const localVue = createLocalVue();
localVue.use(VueApollo);
Vue.use(VueApollo);
describe('WorkItemsHierarchy App', () => {
let wrapper;
const createComponent = (props = {}, data = {}) => {
wrapper = extendedWrapper(
mount(App, {
localVue,
provide: {
illustrationPath: '/foo.svg',
licensePlan: 'free',

View File

@ -1,4 +1,5 @@
import { createLocalVue, mount } from '@vue/test-utils';
import { mount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { GlBadge } from '@gitlab/ui';
import Hierarchy from '~/work_items_hierarchy/components/hierarchy.vue';
@ -6,8 +7,7 @@ import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import RESPONSE from '~/work_items_hierarchy/static_response';
import { workItemTypes } from '~/work_items_hierarchy/constants';
const localVue = createLocalVue();
localVue.use(VueApollo);
Vue.use(VueApollo);
describe('WorkItemsHierarchy Hierarchy', () => {
let wrapper;
@ -32,7 +32,6 @@ describe('WorkItemsHierarchy Hierarchy', () => {
const createComponent = (props = {}) => {
wrapper = extendedWrapper(
mount(Hierarchy, {
localVue,
propsData: {
workItemTypes: props.workItemTypes,
...props,

View File

@ -195,8 +195,8 @@ RSpec.describe GitlabSchema.types['Project'] do
expect(secure_analyzers['type']).to eq('string')
expect(secure_analyzers['field']).to eq('SECURE_ANALYZERS_PREFIX')
expect(secure_analyzers['label']).to eq('Image prefix')
expect(secure_analyzers['defaultValue']).to eq(secure_analyzers_prefix)
expect(secure_analyzers['value']).to eq(secure_analyzers_prefix)
expect(secure_analyzers['defaultValue']).to eq('$TEMPLATE_REGISTRY_HOST/security-products')
expect(secure_analyzers['value']).to eq('$TEMPLATE_REGISTRY_HOST/security-products')
expect(secure_analyzers['size']).to eq('LARGE')
expect(secure_analyzers['options']).to be_nil
end

View File

@ -1,54 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::BackfillCiRunnerSemver, :migration, schema: 20220601151900 do
let(:ci_runners) { table(:ci_runners, database: :ci) }
subject do
described_class.new(
start_id: 10,
end_id: 15,
batch_table: :ci_runners,
batch_column: :id,
sub_batch_size: 10,
pause_ms: 0,
connection: Ci::ApplicationRecord.connection)
end
describe '#perform' do
it 'populates semver column on all runners in range' do
ci_runners.create!(id: 10, runner_type: 1, version: %q(HEAD-fd84d97))
ci_runners.create!(id: 11, runner_type: 1, version: %q(v1.2.3))
ci_runners.create!(id: 12, runner_type: 1, version: %q(2.1.0))
ci_runners.create!(id: 13, runner_type: 1, version: %q(11.8.0~beta.935.g7f6d2abc))
ci_runners.create!(id: 14, runner_type: 1, version: %q(13.2.2/1.1.0))
ci_runners.create!(id: 15, runner_type: 1, version: %q('14.3.4'))
subject.perform
expect(ci_runners.all).to contain_exactly(
an_object_having_attributes(id: 10, semver: nil),
an_object_having_attributes(id: 11, semver: '1.2.3'),
an_object_having_attributes(id: 12, semver: '2.1.0'),
an_object_having_attributes(id: 13, semver: '11.8.0'),
an_object_having_attributes(id: 14, semver: '13.2.2'),
an_object_having_attributes(id: 15, semver: '14.3.4')
)
end
it 'skips runners that already have semver value' do
ci_runners.create!(id: 10, runner_type: 1, version: %q(1.2.4), semver: '1.2.3')
ci_runners.create!(id: 11, runner_type: 1, version: %q(1.2.5))
ci_runners.create!(id: 12, runner_type: 1, version: %q(HEAD), semver: '1.2.4')
subject.perform
expect(ci_runners.all).to contain_exactly(
an_object_having_attributes(id: 10, semver: '1.2.3'),
an_object_having_attributes(id: 11, semver: '1.2.5'),
an_object_having_attributes(id: 12, semver: '1.2.4')
)
end
end
end

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::GithubImport::Importer::Events::BaseImporter do
let(:project) { instance_double('Project') }
let(:user_finder) { instance_double('Gitlab::GithubImport::UserFinder') }
let(:issue_event) { instance_double('Gitlab::GithubImport::Representation::IssueEvent') }
let(:importer_class) { Class.new(described_class) }
let(:importer_instance) { importer_class.new(project, user_finder) }
describe '#execute' do
it { expect { importer_instance.execute(issue_event) }.to raise_error(NotImplementedError) }
end
end

View File

@ -0,0 +1,91 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::GithubImport::Importer::Events::ChangedAssignee do
subject(:importer) { described_class.new(project, user_finder) }
let_it_be(:project) { create(:project, :repository) }
let_it_be(:assignee) { create(:user) }
let_it_be(:assigner) { create(:user) }
let(:client) { instance_double('Gitlab::GithubImport::Client') }
let(:user_finder) { Gitlab::GithubImport::UserFinder.new(project, client) }
let(:issue) { create(:issue, project: project) }
let(:issue_event) do
Gitlab::GithubImport::Representation::IssueEvent.from_json_hash(
'id' => 6501124486,
'actor' => { 'id' => 4, 'login' => 'alice' },
'event' => event_type,
'commit_id' => nil,
'created_at' => '2022-04-26 18:30:53 UTC',
'assigner' => { 'id' => assigner.id, 'login' => assigner.username },
'assignee' => { 'id' => assignee.id, 'login' => assignee.username },
'issue_db_id' => issue.id
)
end
let(:note_attrs) do
{
noteable_id: issue.id,
noteable_type: Issue.name,
project_id: project.id,
author_id: assigner.id,
system: true,
created_at: issue_event.created_at,
updated_at: issue_event.created_at
}.stringify_keys
end
let(:expected_system_note_metadata_attrs) do
{
action: "assignee",
created_at: issue_event.created_at,
updated_at: issue_event.created_at
}.stringify_keys
end
shared_examples 'new note' do
it 'creates expected note' do
expect { importer.execute(issue_event) }.to change { issue.notes.count }
.from(0).to(1)
expect(issue.notes.last)
.to have_attributes(expected_note_attrs)
end
it 'creates expected system note metadata' do
expect { importer.execute(issue_event) }.to change { SystemNoteMetadata.count }
.from(0).to(1)
expect(SystemNoteMetadata.last)
.to have_attributes(
expected_system_note_metadata_attrs.merge(
note_id: Note.last.id
)
)
end
end
describe '#execute' do
before do
allow(user_finder).to receive(:find).with(assignee.id, assignee.username).and_return(assignee.id)
allow(user_finder).to receive(:find).with(assigner.id, assigner.username).and_return(assigner.id)
end
context 'when importing an assigned event' do
let(:event_type) { 'assigned' }
let(:expected_note_attrs) { note_attrs.merge(note: "assigned to @#{assignee.username}") }
it_behaves_like 'new note'
end
context 'when importing an unassigned event' do
let(:event_type) { 'unassigned' }
let(:expected_note_attrs) { note_attrs.merge(note: "unassigned @#{assigner.username}") }
it_behaves_like 'new note'
end
end
end

View File

@ -3,18 +3,20 @@
require 'spec_helper'
RSpec.describe Gitlab::GithubImport::Importer::Events::ChangedLabel do
subject(:importer) { described_class.new(project, user.id) }
subject(:importer) { described_class.new(project, user_finder) }
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
let(:client) { instance_double('Gitlab::GithubImport::Client') }
let(:user_finder) { Gitlab::GithubImport::UserFinder.new(project, client) }
let(:issue) { create(:issue, project: project) }
let!(:label) { create(:label, project: project) }
let(:issue_event) do
Gitlab::GithubImport::Representation::IssueEvent.from_json_hash(
'id' => 6501124486,
'actor' => { 'id' => 4, 'login' => 'alice' },
'actor' => { 'id' => user.id, 'login' => user.username },
'event' => event_type,
'commit_id' => nil,
'label_title' => label.title,
@ -43,6 +45,7 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::ChangedLabel do
before do
allow(Gitlab::Cache::Import::Caching).to receive(:read_integer).and_return(label.id)
allow(user_finder).to receive(:find).with(user.id, user.username).and_return(user.id)
end
context 'when importing a labeled event' do

View File

@ -3,18 +3,20 @@
require 'spec_helper'
RSpec.describe Gitlab::GithubImport::Importer::Events::ChangedMilestone do
subject(:importer) { described_class.new(project, user.id) }
subject(:importer) { described_class.new(project, user_finder) }
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
let(:client) { instance_double('Gitlab::GithubImport::Client') }
let(:user_finder) { Gitlab::GithubImport::UserFinder.new(project, client) }
let(:issue) { create(:issue, project: project) }
let!(:milestone) { create(:milestone, project: project) }
let(:issue_event) do
Gitlab::GithubImport::Representation::IssueEvent.from_json_hash(
'id' => 6501124486,
'actor' => { 'id' => 4, 'login' => 'alice' },
'actor' => { 'id' => user.id, 'login' => user.username },
'event' => event_type,
'commit_id' => nil,
'milestone_title' => milestone.title,
@ -45,6 +47,7 @@ RSpec.describe Gitlab::GithubImport::Importer::Events::ChangedMilestone do
describe '#execute' do
before do
allow(Gitlab::Cache::Import::Caching).to receive(:read_integer).and_return(milestone.id)
allow(user_finder).to receive(:find).with(user.id, user.username).and_return(user.id)
end
context 'when importing a milestoned event' do

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