Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
a09c6d7e91
commit
5d41ea8c8e
|
|
@ -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>
|
||||
|
|
@ -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();
|
||||
};
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
mutation mergeRequestRemoveAttentionRequest($projectPath: ID!, $iid: String!, $userId: UserID!) {
|
||||
mergeRequestRemoveAttentionRequest(
|
||||
input: { projectPath: $projectPath, iid: $iid, userId: $userId }
|
||||
) {
|
||||
errors
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
mutation mergeRequestRequestAttention($projectPath: ID!, $iid: String!, $userId: UserID!) {
|
||||
mergeRequestRequestAttention(input: { projectPath: $projectPath, iid: $iid, userId: $userId }) {
|
||||
errors
|
||||
}
|
||||
}
|
||||
|
|
@ -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(),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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: []
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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} }
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
@ -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')
|
||||
|
|
@ -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} }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1 @@
|
|||
2d5bf23684afbd4dbf3251c4886c22eaaa144332901c1183bc474772f065c54f
|
||||
|
|
@ -0,0 +1 @@
|
|||
c9b214fd49c97d17f43faef4d86b811ea2ad5f573c3cb4a6725de8ee4c92262a
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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: ""
|
||||
|
|
|
|||
|
|
@ -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: ""
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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: ""
|
||||
|
|
|
|||
|
|
@ -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: ""
|
||||
|
|
|
|||
|
|
@ -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: ""
|
||||
|
|
|
|||
|
|
@ -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: ""
|
||||
|
|
|
|||
|
|
@ -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: ""
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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 ""
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
@ -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']);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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.',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in New Issue