Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
17e561ffb8
commit
73ff43129b
|
@ -135,7 +135,6 @@ linters:
|
|||
- Style/NegatedIf
|
||||
- Style/NestedTernaryOperator
|
||||
- Style/ParenthesesAroundCondition
|
||||
- Style/RedundantParentheses
|
||||
- Style/SelfAssignment
|
||||
- Style/TernaryParentheses
|
||||
- Style/TrailingCommaInHashLiteral
|
||||
|
|
2
Gemfile
2
Gemfile
|
@ -2,7 +2,7 @@
|
|||
|
||||
source 'https://rubygems.org'
|
||||
|
||||
gem 'rails', '~> 6.0.3.1'
|
||||
gem 'rails', '~> 6.0.3.6'
|
||||
|
||||
gem 'bootsnap', '~> 1.4.6'
|
||||
|
||||
|
|
107
Gemfile.lock
107
Gemfile.lock
|
@ -17,59 +17,59 @@ GEM
|
|||
abstract_type (0.0.7)
|
||||
acme-client (2.0.6)
|
||||
faraday (>= 0.17, < 2.0.0)
|
||||
actioncable (6.0.3.4)
|
||||
actionpack (= 6.0.3.4)
|
||||
actioncable (6.0.3.6)
|
||||
actionpack (= 6.0.3.6)
|
||||
nio4r (~> 2.0)
|
||||
websocket-driver (>= 0.6.1)
|
||||
actionmailbox (6.0.3.4)
|
||||
actionpack (= 6.0.3.4)
|
||||
activejob (= 6.0.3.4)
|
||||
activerecord (= 6.0.3.4)
|
||||
activestorage (= 6.0.3.4)
|
||||
activesupport (= 6.0.3.4)
|
||||
actionmailbox (6.0.3.6)
|
||||
actionpack (= 6.0.3.6)
|
||||
activejob (= 6.0.3.6)
|
||||
activerecord (= 6.0.3.6)
|
||||
activestorage (= 6.0.3.6)
|
||||
activesupport (= 6.0.3.6)
|
||||
mail (>= 2.7.1)
|
||||
actionmailer (6.0.3.4)
|
||||
actionpack (= 6.0.3.4)
|
||||
actionview (= 6.0.3.4)
|
||||
activejob (= 6.0.3.4)
|
||||
actionmailer (6.0.3.6)
|
||||
actionpack (= 6.0.3.6)
|
||||
actionview (= 6.0.3.6)
|
||||
activejob (= 6.0.3.6)
|
||||
mail (~> 2.5, >= 2.5.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
actionpack (6.0.3.4)
|
||||
actionview (= 6.0.3.4)
|
||||
activesupport (= 6.0.3.4)
|
||||
actionpack (6.0.3.6)
|
||||
actionview (= 6.0.3.6)
|
||||
activesupport (= 6.0.3.6)
|
||||
rack (~> 2.0, >= 2.0.8)
|
||||
rack-test (>= 0.6.3)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
||||
actiontext (6.0.3.4)
|
||||
actionpack (= 6.0.3.4)
|
||||
activerecord (= 6.0.3.4)
|
||||
activestorage (= 6.0.3.4)
|
||||
activesupport (= 6.0.3.4)
|
||||
actiontext (6.0.3.6)
|
||||
actionpack (= 6.0.3.6)
|
||||
activerecord (= 6.0.3.6)
|
||||
activestorage (= 6.0.3.6)
|
||||
activesupport (= 6.0.3.6)
|
||||
nokogiri (>= 1.8.5)
|
||||
actionview (6.0.3.4)
|
||||
activesupport (= 6.0.3.4)
|
||||
actionview (6.0.3.6)
|
||||
activesupport (= 6.0.3.6)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
||||
activejob (6.0.3.4)
|
||||
activesupport (= 6.0.3.4)
|
||||
activejob (6.0.3.6)
|
||||
activesupport (= 6.0.3.6)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (6.0.3.4)
|
||||
activesupport (= 6.0.3.4)
|
||||
activerecord (6.0.3.4)
|
||||
activemodel (= 6.0.3.4)
|
||||
activesupport (= 6.0.3.4)
|
||||
activemodel (6.0.3.6)
|
||||
activesupport (= 6.0.3.6)
|
||||
activerecord (6.0.3.6)
|
||||
activemodel (= 6.0.3.6)
|
||||
activesupport (= 6.0.3.6)
|
||||
activerecord-explain-analyze (0.1.0)
|
||||
activerecord (>= 4)
|
||||
pg
|
||||
activestorage (6.0.3.4)
|
||||
actionpack (= 6.0.3.4)
|
||||
activejob (= 6.0.3.4)
|
||||
activerecord (= 6.0.3.4)
|
||||
marcel (~> 0.3.1)
|
||||
activesupport (6.0.3.4)
|
||||
activestorage (6.0.3.6)
|
||||
actionpack (= 6.0.3.6)
|
||||
activejob (= 6.0.3.6)
|
||||
activerecord (= 6.0.3.6)
|
||||
marcel (~> 1.0.0)
|
||||
activesupport (6.0.3.6)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
i18n (>= 0.7, < 2)
|
||||
minitest (~> 5.1)
|
||||
|
@ -728,8 +728,7 @@ GEM
|
|||
lumberjack (1.2.7)
|
||||
mail (2.7.1)
|
||||
mini_mime (>= 0.1.1)
|
||||
marcel (0.3.3)
|
||||
mimemagic (~> 0.3.2)
|
||||
marcel (1.0.1)
|
||||
marginalia (1.10.0)
|
||||
actionpack (>= 2.3)
|
||||
activerecord (>= 2.3)
|
||||
|
@ -961,20 +960,20 @@ GEM
|
|||
rack-test (1.1.0)
|
||||
rack (>= 1.0, < 3)
|
||||
rack-timeout (0.5.2)
|
||||
rails (6.0.3.4)
|
||||
actioncable (= 6.0.3.4)
|
||||
actionmailbox (= 6.0.3.4)
|
||||
actionmailer (= 6.0.3.4)
|
||||
actionpack (= 6.0.3.4)
|
||||
actiontext (= 6.0.3.4)
|
||||
actionview (= 6.0.3.4)
|
||||
activejob (= 6.0.3.4)
|
||||
activemodel (= 6.0.3.4)
|
||||
activerecord (= 6.0.3.4)
|
||||
activestorage (= 6.0.3.4)
|
||||
activesupport (= 6.0.3.4)
|
||||
rails (6.0.3.6)
|
||||
actioncable (= 6.0.3.6)
|
||||
actionmailbox (= 6.0.3.6)
|
||||
actionmailer (= 6.0.3.6)
|
||||
actionpack (= 6.0.3.6)
|
||||
actiontext (= 6.0.3.6)
|
||||
actionview (= 6.0.3.6)
|
||||
activejob (= 6.0.3.6)
|
||||
activemodel (= 6.0.3.6)
|
||||
activerecord (= 6.0.3.6)
|
||||
activestorage (= 6.0.3.6)
|
||||
activesupport (= 6.0.3.6)
|
||||
bundler (>= 1.3.0)
|
||||
railties (= 6.0.3.4)
|
||||
railties (= 6.0.3.6)
|
||||
sprockets-rails (>= 2.0.0)
|
||||
rails-controller-testing (1.0.5)
|
||||
actionpack (>= 5.0.1.rc1)
|
||||
|
@ -988,9 +987,9 @@ GEM
|
|||
rails-i18n (6.0.0)
|
||||
i18n (>= 0.7, < 2)
|
||||
railties (>= 6.0.0, < 7)
|
||||
railties (6.0.3.4)
|
||||
actionpack (= 6.0.3.4)
|
||||
activesupport (= 6.0.3.4)
|
||||
railties (6.0.3.6)
|
||||
actionpack (= 6.0.3.6)
|
||||
activesupport (= 6.0.3.6)
|
||||
method_source
|
||||
rake (>= 0.8.7)
|
||||
thor (>= 0.20.3, < 2.0)
|
||||
|
@ -1548,7 +1547,7 @@ DEPENDENCIES
|
|||
rack-oauth2 (~> 1.16.0)
|
||||
rack-proxy (~> 0.6.0)
|
||||
rack-timeout (~> 0.5.1)
|
||||
rails (~> 6.0.3.1)
|
||||
rails (~> 6.0.3.6)
|
||||
rails-controller-testing
|
||||
rails-i18n (~> 6.0)
|
||||
rainbow (~> 3.0)
|
||||
|
|
|
@ -163,15 +163,9 @@ export default () => {
|
|||
eventHub.$off('initialBoardLoad', this.initialBoardLoad);
|
||||
},
|
||||
mounted() {
|
||||
if (!gon.features?.boardsFilteredSearch) {
|
||||
this.filterManager = new FilteredSearchBoards(
|
||||
boardsStore.filter,
|
||||
true,
|
||||
boardsStore.cantEdit,
|
||||
);
|
||||
this.filterManager = new FilteredSearchBoards(boardsStore.filter, true, boardsStore.cantEdit);
|
||||
|
||||
this.filterManager.setup();
|
||||
}
|
||||
this.filterManager.setup();
|
||||
|
||||
this.performSearch();
|
||||
|
||||
|
|
|
@ -42,6 +42,11 @@ export default {
|
|||
required: false,
|
||||
default: false,
|
||||
},
|
||||
oncallSchedules: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
|
@ -52,6 +57,9 @@ export default {
|
|||
computedMemberPath() {
|
||||
return this.memberPath.replace(':id', this.memberId);
|
||||
},
|
||||
stringifiedSchedules() {
|
||||
return JSON.stringify(this.oncallSchedules);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -69,6 +77,7 @@ export default {
|
|||
:data-is-access-request="isAccessRequest"
|
||||
:data-is-invite="isInvite"
|
||||
:data-message="message"
|
||||
:data-oncall-schedules="stringifiedSchedules"
|
||||
data-qa-selector="delete_member_button"
|
||||
/>
|
||||
</template>
|
||||
|
|
|
@ -33,7 +33,7 @@ export default {
|
|||
|
||||
if (user) {
|
||||
return sprintf(
|
||||
s__('Members|Are you sure you want to remove %{usersName} from "%{source}"'),
|
||||
s__('Members|Are you sure you want to remove %{usersName} from "%{source}"?'),
|
||||
{
|
||||
usersName: user.name,
|
||||
source: source.fullName,
|
||||
|
@ -42,12 +42,16 @@ export default {
|
|||
}
|
||||
|
||||
return sprintf(
|
||||
s__('Members|Are you sure you want to remove this orphaned member from "%{source}"'),
|
||||
s__('Members|Are you sure you want to remove this orphaned member from "%{source}"?'),
|
||||
{
|
||||
source: source.fullName,
|
||||
},
|
||||
);
|
||||
},
|
||||
oncallScheduleUserData() {
|
||||
const { user: { name, oncallSchedules: schedules } = {} } = this.member;
|
||||
return { name, schedules };
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -60,6 +64,7 @@ export default {
|
|||
v-else
|
||||
:member-id="member.id"
|
||||
:member-type="member.type"
|
||||
:oncall-schedules="oncallScheduleUserData"
|
||||
:message="message"
|
||||
:title="s__('Member|Remove member')"
|
||||
/>
|
||||
|
|
|
@ -3,6 +3,7 @@ import { GlModal, GlForm, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
|
|||
import { mapState } from 'vuex';
|
||||
import csrf from '~/lib/utils/csrf';
|
||||
import { __, s__, sprintf } from '~/locale';
|
||||
import OncallSchedulesList from '~/vue_shared/components/oncall_schedules_list.vue';
|
||||
import { LEAVE_MODAL_ID } from '../../constants';
|
||||
|
||||
export default {
|
||||
|
@ -19,7 +20,7 @@ export default {
|
|||
csrf,
|
||||
modalId: LEAVE_MODAL_ID,
|
||||
modalContent: s__('Members|Are you sure you want to leave "%{source}"?'),
|
||||
components: { GlModal, GlForm, GlSprintf },
|
||||
components: { GlModal, GlForm, GlSprintf, OncallSchedulesList },
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
|
@ -42,6 +43,12 @@ export default {
|
|||
modalTitle() {
|
||||
return sprintf(s__('Members|Leave "%{source}"'), { source: this.member.source.fullName });
|
||||
},
|
||||
schedules() {
|
||||
return this.member.user?.oncallSchedules;
|
||||
},
|
||||
isPartOfOnCallSchedules() {
|
||||
return this.schedules?.length;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handlePrimary() {
|
||||
|
@ -58,7 +65,6 @@ export default {
|
|||
:title="modalTitle"
|
||||
:action-primary="$options.actionPrimary"
|
||||
:action-cancel="$options.actionCancel"
|
||||
size="sm"
|
||||
@primary="handlePrimary"
|
||||
>
|
||||
<gl-form ref="form" :action="leavePath" method="post">
|
||||
|
@ -68,6 +74,12 @@ export default {
|
|||
</gl-sprintf>
|
||||
</p>
|
||||
|
||||
<oncall-schedules-list
|
||||
v-if="isPartOfOnCallSchedules"
|
||||
:schedules="schedules"
|
||||
:is-current-user="true"
|
||||
/>
|
||||
|
||||
<input type="hidden" name="_method" value="delete" />
|
||||
<input :value="$options.csrf.token" type="hidden" name="authenticity_token" />
|
||||
</gl-form>
|
||||
|
|
|
@ -514,7 +514,7 @@ export default {
|
|||
>
|
||||
{{
|
||||
s__(
|
||||
'mrWidget|Fork merge requests do not create merge request pipelines which validate a post merge result',
|
||||
'mrWidget|If the last pipeline ran in the fork project, it may be inaccurate. Before merge, we advise running a pipeline in this project.',
|
||||
)
|
||||
}}
|
||||
</mr-widget-alert-message>
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
<script>
|
||||
/* eslint-disable vue/no-v-html */
|
||||
import { GlIcon } from '@gitlab/ui';
|
||||
import EditorLite from '~/vue_shared/components/editor_lite.vue';
|
||||
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import { HIGHLIGHT_CLASS_NAME } from './constants';
|
||||
import ViewerMixin from './mixins';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlIcon,
|
||||
EditorLite,
|
||||
EditorLite: () =>
|
||||
import(/* webpackChunkName: 'EditorLite' */ '~/vue_shared/components/editor_lite.vue'),
|
||||
},
|
||||
mixins: [ViewerMixin],
|
||||
mixins: [ViewerMixin, glFeatureFlagsMixin()],
|
||||
inject: ['blobHash'],
|
||||
data() {
|
||||
return {
|
||||
|
@ -21,6 +22,9 @@ export default {
|
|||
lineNumbers() {
|
||||
return this.content.split('\n').length;
|
||||
},
|
||||
refactorBlobViewerEnabled() {
|
||||
return this.glFeatures.refactorBlobViewer;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
const { hash } = window.location;
|
||||
|
@ -49,7 +53,7 @@ export default {
|
|||
<template>
|
||||
<div>
|
||||
<editor-lite
|
||||
v-if="isRawContent"
|
||||
v-if="isRawContent && refactorBlobViewerEnabled"
|
||||
:value="content"
|
||||
:file-name="fileName"
|
||||
:editor-options="{ readOnly: true }"
|
||||
|
|
|
@ -147,6 +147,7 @@ export default {
|
|||
</gl-button>
|
||||
<apply-suggestion
|
||||
v-if="isLoggedIn"
|
||||
v-gl-tooltip.viewport="tooltipMessage"
|
||||
:disabled="isDisableButton"
|
||||
:default-commit-message="defaultCommitMessage"
|
||||
class="gl-ml-3"
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
<script>
|
||||
import { GlSprintf, GlLink } from '@gitlab/ui';
|
||||
import { sprintf, s__ } from '~/locale';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlSprintf,
|
||||
GlLink,
|
||||
},
|
||||
props: {
|
||||
schedules: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
userName: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
isCurrentUser: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
title() {
|
||||
return this.isCurrentUser
|
||||
? s__('OnCallSchedules|You are currently a part of:')
|
||||
: sprintf(s__('OnCallSchedules|User %{name} is currently part of:'), {
|
||||
name: this.userName,
|
||||
});
|
||||
},
|
||||
footer() {
|
||||
return this.isCurrentUser
|
||||
? s__(
|
||||
'OnCallSchedules|Removing yourself may put your on-call team at risk of missing a notification.',
|
||||
)
|
||||
: s__(
|
||||
'OnCallSchedules|Removing this user may put their on-call team at risk of missing a notification.',
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<p data-testid="title">{{ title }}</p>
|
||||
|
||||
<ul data-testid="schedules-list">
|
||||
<li v-for="(schedule, index) in schedules" :key="`${schedule.name}-${index}`">
|
||||
<gl-sprintf
|
||||
:message="s__('OnCallSchedules|On-call schedule %{schedule} in Project %{project}')"
|
||||
>
|
||||
<template #schedule>
|
||||
<gl-link :href="schedule.scheduleUrl" target="_blank">{{ schedule.name }}</gl-link>
|
||||
</template>
|
||||
<template #project>
|
||||
<gl-link :href="schedule.projectUrl" target="_blank">{{
|
||||
schedule.projectName
|
||||
}}</gl-link>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p data-testid="footer">{{ footer }}</p>
|
||||
</div>
|
||||
</template>
|
|
@ -1,8 +1,10 @@
|
|||
<script>
|
||||
import { GlFormCheckbox, GlModal } from '@gitlab/ui';
|
||||
import * as Sentry from '@sentry/browser';
|
||||
import { parseBoolean } from '~/lib/utils/common_utils';
|
||||
import csrf from '~/lib/utils/csrf';
|
||||
import { s__, __ } from '~/locale';
|
||||
import OncallSchedulesList from '~/vue_shared/components/oncall_schedules_list.vue';
|
||||
|
||||
export default {
|
||||
actionCancel: {
|
||||
|
@ -12,6 +14,7 @@ export default {
|
|||
components: {
|
||||
GlFormCheckbox,
|
||||
GlModal,
|
||||
OncallSchedulesList,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -48,6 +51,18 @@ export default {
|
|||
showUnassignIssuablesCheckbox() {
|
||||
return !this.isAccessRequest && !this.isInvite;
|
||||
},
|
||||
isPartOfOncallSchedules() {
|
||||
return !this.isAccessRequest && this.oncallSchedules.schedules?.length;
|
||||
},
|
||||
oncallSchedules() {
|
||||
let schedules = {};
|
||||
try {
|
||||
schedules = JSON.parse(this.modalData.oncallSchedules);
|
||||
} catch (e) {
|
||||
Sentry.captureException(e);
|
||||
}
|
||||
return schedules;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
document.addEventListener('click', this.handleClick);
|
||||
|
@ -83,6 +98,12 @@ export default {
|
|||
<form ref="form" :action="modalData.memberPath" method="post">
|
||||
<p data-testid="modal-message">{{ modalData.message }}</p>
|
||||
|
||||
<oncall-schedules-list
|
||||
v-if="isPartOfOncallSchedules"
|
||||
:schedules="oncallSchedules.schedules"
|
||||
:user-name="oncallSchedules.name"
|
||||
/>
|
||||
|
||||
<input ref="method" type="hidden" name="_method" value="delete" />
|
||||
<input :value="$options.csrf.token" type="hidden" name="authenticity_token" />
|
||||
<gl-form-checkbox v-if="isGroupMember" name="remove_sub_memberships">
|
||||
|
|
|
@ -26,6 +26,13 @@ gl-emoji {
|
|||
height: 30px;
|
||||
// Create a width that fits 9 emojis per row
|
||||
width: 100 / 9 * 1%;
|
||||
transition: transform 0.15s cubic-bezier(0.3, 0, 0.2, 2) !important;
|
||||
will-change: transform;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
transform: scale(1.3);
|
||||
}
|
||||
}
|
||||
|
||||
.emoji-picker .gl-new-dropdown .dropdown-menu {
|
||||
|
|
|
@ -9,7 +9,6 @@ class Groups::BoardsController < Groups::ApplicationController
|
|||
before_action :assign_endpoint_vars
|
||||
before_action do
|
||||
push_frontend_feature_flag(:graphql_board_lists, group, default_enabled: false)
|
||||
push_frontend_feature_flag(:boards_filtered_search, group)
|
||||
push_frontend_feature_flag(:swimlanes_buffered_rendering, group, default_enabled: :yaml)
|
||||
end
|
||||
|
||||
|
|
|
@ -346,6 +346,10 @@ class Group < Namespace
|
|||
members_with_parents.owners.exists?(user_id: user)
|
||||
end
|
||||
|
||||
def blocked_owners
|
||||
members.blocked.where(access_level: Gitlab::Access::OWNER)
|
||||
end
|
||||
|
||||
def has_maintainer?(user)
|
||||
return false unless user
|
||||
|
||||
|
@ -358,14 +362,29 @@ class Group < Namespace
|
|||
|
||||
# Check if user is a last owner of the group.
|
||||
def last_owner?(user)
|
||||
has_owner?(user) && members_with_parents.owners.size == 1
|
||||
has_owner?(user) && single_owner?
|
||||
end
|
||||
|
||||
def last_blocked_owner?(user)
|
||||
def member_last_owner?(member)
|
||||
return member.last_owner unless member.last_owner.nil?
|
||||
|
||||
last_owner?(member.user)
|
||||
end
|
||||
|
||||
def single_owner?
|
||||
members_with_parents.owners.size == 1
|
||||
end
|
||||
|
||||
def single_blocked_owner?
|
||||
blocked_owners.size == 1
|
||||
end
|
||||
|
||||
def member_last_blocked_owner?(member)
|
||||
return member.last_blocked_owner unless member.last_blocked_owner.nil?
|
||||
|
||||
return false if members_with_parents.owners.any?
|
||||
|
||||
blocked_owners = members.blocked.where(access_level: Gitlab::Access::OWNER)
|
||||
blocked_owners.size == 1 && blocked_owners.exists?(user_id: user)
|
||||
single_blocked_owner? && blocked_owners.exists?(user_id: member.user)
|
||||
end
|
||||
|
||||
def ldap_synced?
|
||||
|
|
|
@ -26,6 +26,8 @@ class GroupMember < Member
|
|||
after_create :update_two_factor_requirement, unless: :invite?
|
||||
after_destroy :update_two_factor_requirement, unless: :invite?
|
||||
|
||||
attr_accessor :last_owner, :last_blocked_owner
|
||||
|
||||
def self.access_level_roles
|
||||
Gitlab::Access.options_with_owner
|
||||
end
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Members
|
||||
class LastGroupOwnerAssigner
|
||||
def initialize(group, members)
|
||||
@group = group
|
||||
@members = members
|
||||
end
|
||||
|
||||
def execute
|
||||
@last_blocked_owner = no_owners_in_heirarchy? && group.single_blocked_owner?
|
||||
@group_single_owner = owners.size == 1
|
||||
|
||||
members.each { |member| set_last_owner(member) }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :group, :members, :last_blocked_owner, :group_single_owner
|
||||
|
||||
def no_owners_in_heirarchy?
|
||||
owners.empty?
|
||||
end
|
||||
|
||||
def set_last_owner(member)
|
||||
member.last_owner = member.id.in?(owner_ids) && group_single_owner
|
||||
member.last_blocked_owner = member.id.in?(blocked_owner_ids) && last_blocked_owner
|
||||
end
|
||||
|
||||
def owner_ids
|
||||
@owner_ids ||= owners.where(id: member_ids).ids
|
||||
end
|
||||
|
||||
def blocked_owner_ids
|
||||
@blocked_owner_ids ||= group.blocked_owners.where(id: member_ids).ids
|
||||
end
|
||||
|
||||
def member_ids
|
||||
@members_ids ||= members.pluck(:id)
|
||||
end
|
||||
|
||||
def owners
|
||||
@owners ||= group.members_with_parents.owners.load
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,6 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Packages::Debian::FileMetadatum < ApplicationRecord
|
||||
self.primary_key = :package_file_id
|
||||
|
||||
belongs_to :package_file, inverse_of: :debian_file_metadatum
|
||||
|
||||
validates :package_file, presence: true
|
||||
|
|
|
@ -19,6 +19,7 @@ class Packages::Maven::Metadatum < ApplicationRecord
|
|||
validate :maven_package_type
|
||||
|
||||
scope :for_package_ids, -> (package_ids) { where(package_id: package_ids) }
|
||||
scope :with_path, ->(path) { where(path: path) }
|
||||
scope :order_created, -> { reorder('created_at ASC') }
|
||||
|
||||
def self.pluck_app_name
|
||||
|
|
|
@ -4,7 +4,7 @@ class GroupMemberPolicy < BasePolicy
|
|||
delegate :group
|
||||
|
||||
with_scope :subject
|
||||
condition(:last_owner) { @subject.group.last_owner?(@subject.user) || @subject.group.last_blocked_owner?(@subject.user) }
|
||||
condition(:last_owner) { @subject.group.member_last_owner?(@subject) || @subject.group.member_last_blocked_owner?(@subject) }
|
||||
|
||||
desc "Membership is users' own"
|
||||
with_score 0
|
||||
|
|
|
@ -2,4 +2,10 @@
|
|||
|
||||
class MemberSerializer < BaseSerializer
|
||||
entity MemberEntity
|
||||
|
||||
def represent(members, opts = {})
|
||||
Members::LastGroupOwnerAssigner.new(opts[:group], members).execute unless opts[:source].is_a?(Project)
|
||||
|
||||
super(members, opts)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-account-settings'), html: { class: 'fieldset-form' } do |f|
|
||||
= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-account-settings'), html: { class: 'fieldset-form', id: 'account-settings' } do |f|
|
||||
= form_errors(@application_setting)
|
||||
|
||||
%fieldset
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-merge-request-settings'), html: { class: 'fieldset-form' } do |f|
|
||||
= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-merge-request-settings'), html: { class: 'fieldset-form', id: 'merge-request-settings' } do |f|
|
||||
= form_errors(@application_setting)
|
||||
|
||||
%fieldset
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
= _('Amazon EKS integration allows you to provision EKS clusters from GitLab.')
|
||||
|
||||
.settings-content
|
||||
= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-eks-settings'), html: { class: 'fieldset-form' } do |f|
|
||||
= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-eks-settings'), html: { class: 'fieldset-form', id: 'eks-settings' } do |f|
|
||||
= form_errors(@application_setting)
|
||||
|
||||
%fieldset
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
= _('External Classification Policy Authorization')
|
||||
.settings-content
|
||||
|
||||
= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-external-auth-settings'), html: { class: 'fieldset-form' } do |f|
|
||||
= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-external-auth-settings'), html: { class: 'fieldset-form', id: 'external-auth-settings' } do |f|
|
||||
= form_errors(@application_setting)
|
||||
|
||||
%fieldset
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
|
||||
|
||||
.settings-content
|
||||
= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-gitpod-settings'), html: { class: 'fieldset-form' } do |f|
|
||||
= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-gitpod-settings'), html: { class: 'fieldset-form', id: 'gitpod-settings' } do |f|
|
||||
= form_errors(@application_setting)
|
||||
|
||||
%fieldset
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
%p
|
||||
= _('Allow rendering of diagrams in AsciiDoc and Markdown documents using %{link}.').html_safe % { link: link_to('Kroki', 'https://kroki.io', target: '_blank') }
|
||||
.settings-content
|
||||
= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-kroki-settings'), html: { class: 'fieldset-form' } do |f|
|
||||
= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-kroki-settings'), html: { class: 'fieldset-form', id: 'kroki-settings' } do |f|
|
||||
= form_errors(@application_setting) if expanded
|
||||
|
||||
%fieldset
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
%p
|
||||
= _('Allow rendering of PlantUML diagrams in Asciidoc documents.')
|
||||
.settings-content
|
||||
= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-plantuml-settings'), html: { class: 'fieldset-form' } do |f|
|
||||
= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-plantuml-settings'), html: { class: 'fieldset-form', id: 'plantuml-settings' } do |f|
|
||||
= form_errors(@application_setting) if expanded
|
||||
|
||||
%fieldset
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-signin-settings'), html: { class: 'fieldset-form' } do |f|
|
||||
= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-signin-settings'), html: { class: 'fieldset-form', id: 'signin-settings' } do |f|
|
||||
= form_errors(@application_setting)
|
||||
|
||||
%fieldset
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
%p
|
||||
= _('Configure the %{link} integration.').html_safe % { link: link_to('Snowplow', 'https://snowplowanalytics.com/', target: '_blank') }
|
||||
.settings-content
|
||||
= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-snowplow-settings'), html: { class: 'fieldset-form' } do |f|
|
||||
= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-snowplow-settings'), html: { class: 'fieldset-form', id: 'snowplow-settings' } do |f|
|
||||
= form_errors(@application_setting) if expanded
|
||||
|
||||
%fieldset
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
|
||||
.settings-content
|
||||
= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-sourcegraph-settings'), html: { class: 'fieldset-form' } do |f|
|
||||
= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-sourcegraph-settings'), html: { class: 'fieldset-form', id: 'sourcegraph-settings' } do |f|
|
||||
= form_errors(@application_setting)
|
||||
|
||||
%fieldset
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-terminal-settings'), html: { class: 'fieldset-form' } do |f|
|
||||
= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-terminal-settings'), html: { class: 'fieldset-form', id: 'terminal-settings' } do |f|
|
||||
= form_errors(@application_setting)
|
||||
|
||||
%fieldset
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-terms-settings'), html: { class: 'fieldset-form' } do |f|
|
||||
= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-terms-settings'), html: { class: 'fieldset-form', id: 'terms-settings' } do |f|
|
||||
= form_errors(@application_setting)
|
||||
|
||||
%fieldset
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
%p
|
||||
= _('Control the display of third party offers.')
|
||||
.settings-content
|
||||
= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-third-party-offers-settings'), html: { class: 'fieldset-form' } do |f|
|
||||
= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-third-party-offers-settings'), html: { class: 'fieldset-form', id: 'third-party-offers-settings' } do |f|
|
||||
= form_errors(@application_setting) if expanded
|
||||
|
||||
%fieldset
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-visibility-settings'), html: { class: 'fieldset-form' } do |f|
|
||||
= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-visibility-settings'), html: { class: 'fieldset-form', id: 'visibility-settings' } do |f|
|
||||
= form_errors(@application_setting)
|
||||
|
||||
%fieldset
|
||||
|
|
|
@ -90,7 +90,7 @@
|
|||
%p
|
||||
= _('Manage Web IDE features')
|
||||
.settings-content
|
||||
= form_for @application_setting, url: general_admin_application_settings_path(anchor: "#js-web-ide-settings"), html: { class: 'fieldset-form' } do |f|
|
||||
= form_for @application_setting, url: general_admin_application_settings_path(anchor: "#js-web-ide-settings"), html: { class: 'fieldset-form', id: 'web-ide-settings' } do |f|
|
||||
= form_errors(@application_setting)
|
||||
|
||||
%fieldset
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
.awards.js-awards-block{ class: ("hidden" if !inline && grouped_emojis.empty?), data: { award_url: toggle_award_url(awardable) } }
|
||||
- awards_sort(grouped_emojis).each do |emoji, awards|
|
||||
%button.gl-button.btn.btn-default.award-control.js-emoji-btn.has-tooltip{ type: "button",
|
||||
class: [(award_state_class(awardable, awards, current_user))],
|
||||
class: [award_state_class(awardable, awards, current_user)],
|
||||
data: { title: award_user_list(awards, current_user) } }
|
||||
= emoji_icon(emoji)
|
||||
%span.award-control-text.js-counter
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
= render 'shared/namespaces/cascading_settings/lock_popovers'
|
||||
|
||||
%section.settings.gs-general.no-animate#js-general-settings{ class: ('expanded') }
|
||||
%section.settings.gs-general.no-animate.expanded#js-general-settings
|
||||
.settings-header
|
||||
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only{ role: 'button' }
|
||||
= _('Naming, visibility')
|
||||
|
|
|
@ -32,7 +32,7 @@
|
|||
%tbody
|
||||
- @user_map.each do |id, user|
|
||||
%tr
|
||||
%td= (id)
|
||||
%td= id
|
||||
%td= text_field_tag "users[#{id}][name]", user[:name], class: 'form-control'
|
||||
%td= text_field_tag "users[#{id}][email]", user[:email], class: 'form-control'
|
||||
%td
|
||||
|
|
|
@ -2,6 +2,6 @@
|
|||
- if message
|
||||
.flash-container.flash-container-page
|
||||
.flash-notice
|
||||
%div{ class: (container_class) }
|
||||
%div{ class: container_class }
|
||||
%span
|
||||
= message
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
= f.select :role, ::User.roles.keys.map { |role| [role.titleize, role] }, {}, class: 'form-control js-user-role-dropdown', autofocus: true
|
||||
- if Feature.enabled?(:user_other_role_details)
|
||||
.row
|
||||
.form-group.col-sm-12.js-other-role-group{ class: ("hidden") }
|
||||
.form-group.col-sm-12.js-other-role-group.hidden
|
||||
= f.label :other_role, _('What is your job title? (optional)'), class: 'form-check-label gl-mb-3'
|
||||
= f.text_field :other_role, class: 'form-control'
|
||||
= render_if_exists "registrations/welcome/setup_for_company", f: f
|
||||
|
|
|
@ -5,5 +5,5 @@
|
|||
- scopes.each do |scope|
|
||||
%fieldset.form-group.form-check
|
||||
= check_box_tag "#{prefix}[scopes][]", scope, token.scopes.include?(scope), id: "#{prefix}_scopes_#{scope}", class: "form-check-input qa-#{scope}-radio"
|
||||
= label_tag ("#{prefix}_scopes_#{scope}"), scope, class: 'label-bold form-check-label'
|
||||
= label_tag "#{prefix}_scopes_#{scope}", scope, class: 'label-bold form-check-label'
|
||||
.text-secondary= t scope, scope: scope_description(prefix)
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Resolve group_member policy n+1
|
||||
merge_request: 58668
|
||||
author:
|
||||
type: performance
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: "When removing a user, warn Admin user is part of an on-call schedule"
|
||||
merge_request: 57397
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add index for the path column on the packages_maven_metadata table
|
||||
merge_request: 59241
|
||||
author:
|
||||
type: performance
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix tooltip not rendering
|
||||
merge_request: 59202
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Allow a Global ID to be used when filtering issue by iterationId in GraphQL
|
||||
merge_request: 57620
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Update to Rails v6.0.3.6
|
||||
merge_request: 59328
|
||||
author:
|
||||
type: security
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: check_maven_path_first
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/59241
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/327487
|
||||
milestone: '13.11'
|
||||
type: development
|
||||
group: group::package
|
||||
default_enabled: false
|
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddIndexToPackagesMavenMetadataPath < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
INDEX_NAME = 'index_packages_maven_metadata_on_path'
|
||||
|
||||
def up
|
||||
add_concurrent_index :packages_maven_metadata, :path, name: INDEX_NAME
|
||||
end
|
||||
|
||||
def down
|
||||
remove_concurrent_index :packages_maven_metadata, :path, name: INDEX_NAME
|
||||
end
|
||||
end
|
|
@ -0,0 +1 @@
|
|||
2da634fa920e3989d9b8e53ddc1ba005e5bc0f4701426e3841d90a42bd2e908f
|
|
@ -23360,6 +23360,8 @@ CREATE INDEX index_packages_events_on_package_id ON packages_events USING btree
|
|||
|
||||
CREATE INDEX index_packages_maven_metadata_on_package_id_and_path ON packages_maven_metadata USING btree (package_id, path);
|
||||
|
||||
CREATE INDEX index_packages_maven_metadata_on_path ON packages_maven_metadata USING btree (path);
|
||||
|
||||
CREATE INDEX index_packages_nuget_dl_metadata_on_dependency_link_id ON packages_nuget_dependency_link_metadata USING btree (dependency_link_id);
|
||||
|
||||
CREATE UNIQUE INDEX index_packages_on_project_id_name_version_unique_when_generic ON packages_packages USING btree (project_id, name, version) WHERE (package_type = 7);
|
||||
|
|
|
@ -181,6 +181,7 @@ successfully, you must replicate their data using some other means.
|
|||
| [Personal snippets](../../../user/snippets.md) | **Yes** (10.2) | **Yes** (10.2) | No | |
|
||||
| [Project snippets](../../../user/snippets.md) | **Yes** (10.2) | **Yes** (10.2) | No | |
|
||||
| [CI job artifacts (other than Job Logs)](../../../ci/pipelines/job_artifacts.md) | **Yes** (10.4) | [No](https://gitlab.com/gitlab-org/gitlab/-/issues/8923) | Via Object Storage provider if supported. Native Geo support (Beta) . | Verified only manually using [Integrity Check Rake Task](../../raketasks/check.md) on both sites and comparing the output between them |
|
||||
| [CI Pipeline Artifacts](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/models/ci/pipeline_artifact.rb) | [**Yes** (13.11)](https://gitlab.com/gitlab-org/gitlab/-/issues/238464) | [**Yes** (13.11)](https://gitlab.com/gitlab-org/gitlab/-/issues/238464) | Via Object Storage provider if supported. Native Geo support (Beta). | Persists additional artifacts after a pipeline completes |
|
||||
| [Job logs](../../job_logs.md) | **Yes** (10.4) | [No](https://gitlab.com/gitlab-org/gitlab/-/issues/8923) | Via Object Storage provider if supported. Native Geo support (Beta). | Verified only on transfer or manually using [Integrity Check Rake Task](../../raketasks/check.md) on both sites and comparing the output between them |
|
||||
| [Object pools for forked project deduplication](../../../development/git_object_deduplication.md) | **Yes** | No | No | |
|
||||
| [Container Registry](../../packages/container_registry.md) | **Yes** (12.3) | No | No | Disabled by default. See [instructions](docker_registry.md) to enable. |
|
||||
|
@ -199,6 +200,5 @@ successfully, you must replicate their data using some other means.
|
|||
| [Server-side Git hooks](../../server_hooks.md) | [No](https://gitlab.com/groups/gitlab-org/-/epics/1867) | No | No | |
|
||||
| [Elasticsearch integration](../../../integration/elasticsearch.md) | [No](https://gitlab.com/gitlab-org/gitlab/-/issues/1186) | No | No | |
|
||||
| [GitLab Pages](../../pages/index.md) | [No](https://gitlab.com/groups/gitlab-org/-/epics/589) | No | No | |
|
||||
| [CI Pipeline Artifacts](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/models/ci/pipeline_artifact.rb) | [No](https://gitlab.com/gitlab-org/gitlab/-/issues/238464) | No | Via Object Storage provider if supported. Native Geo support (Beta). | Persists additional artifacts after a pipeline completes |
|
||||
| [Dependency proxy images](../../../user/packages/dependency_proxy/index.md) | [No](https://gitlab.com/gitlab-org/gitlab/-/issues/259694) | No | No | Blocked on [Geo: Secondary Mimicry](https://gitlab.com/groups/gitlab-org/-/epics/1528). Note that replication of this cache is not needed for Disaster Recovery purposes because it can be recreated from external sources. |
|
||||
| [Vulnerability Export](../../../user/application_security/vulnerability_report/#export-vulnerability-details) | [Not planned](https://gitlab.com/groups/gitlab-org/-/epics/3111) | No | Via Object Storage provider if supported. Native Geo support (Beta). | Not planned because they are ephemeral and sensitive. They can be regenerated on demand. |
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
---
|
||||
stage: Verify
|
||||
group: Continuous Integration
|
||||
group: Pipeline Authoring
|
||||
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
|
||||
type: reference
|
||||
---
|
||||
|
||||
# Multi-project pipelines
|
||||
# Multi-project pipelines **(FREE)**
|
||||
|
||||
> - [Introduced](https://about.gitlab.com/releases/2015/08/22/gitlab-7-14-released/#build-triggers-api-gitlab-ci) in GitLab 7.14, as Build Triggers.
|
||||
> - [Made available](https://gitlab.com/gitlab-org/gitlab/-/issues/199224) in all tiers in GitLab 12.8.
|
||||
> [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/199224) to GitLab Free in 12.8.
|
||||
|
||||
You can set up [GitLab CI/CD](README.md) across multiple projects, so that a pipeline
|
||||
in one project can trigger a pipeline in another project.
|
||||
|
@ -42,8 +41,6 @@ With Multi-Project Pipelines you can visualize the entire pipeline, including al
|
|||
|
||||
## Multi-project pipeline visualization **(PREMIUM)**
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/2121) in [GitLab Premium 9.3](https://about.gitlab.com/releases/2017/06/22/gitlab-9-3-released/#multi-project-pipeline-graphs).
|
||||
|
||||
When you configure GitLab CI/CD for your project, you can visualize the stages of your
|
||||
[jobs](pipelines/index.md#configure-a-pipeline) on a [pipeline graph](pipelines/index.md#visualize-pipelines).
|
||||
|
||||
|
@ -56,8 +53,7 @@ and when hovering or tapping (on touchscreen devices) they expand and are shown
|
|||
|
||||
## Triggering multi-project pipelines through API
|
||||
|
||||
> - Use of `CI_JOB_TOKEN` for multi-project pipelines was [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/2017) in [GitLab Premium](https://about.gitlab.com/pricing/) 9.3.
|
||||
> - Use of `CI_JOB_TOKEN` for multi-project pipelines was [made available](https://gitlab.com/gitlab-org/gitlab/-/issues/31573) in all tiers in GitLab 12.4.
|
||||
> [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/31573) to GitLab Free in 12.4.
|
||||
|
||||
When you use the [`CI_JOB_TOKEN` to trigger pipelines](triggers/README.md#ci-job-token), GitLab
|
||||
recognizes the source of the job token, and thus internally ties these pipelines
|
||||
|
@ -76,8 +72,7 @@ When using:
|
|||
|
||||
## Creating multi-project pipelines from `.gitlab-ci.yml`
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/8997) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.8.
|
||||
> - [Made available](https://gitlab.com/gitlab-org/gitlab/-/issues/199224) in all tiers in 12.8.
|
||||
> [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/199224) to GitLab Free in 12.8.
|
||||
|
||||
### Triggering a downstream pipeline using a bridge job
|
||||
|
||||
|
@ -260,7 +255,7 @@ test:
|
|||
|
||||
### Mirroring status from triggered pipeline
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/11238) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.3.
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/11238) in GitLab Premium 12.3.
|
||||
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/199224) to GitLab Free in 12.8.
|
||||
|
||||
You can mirror the pipeline status from the triggered pipeline to the source
|
||||
|
@ -309,7 +304,7 @@ Some features are not implemented yet. For example, support for environments.
|
|||
|
||||
## Trigger a pipeline when an upstream project is rebuilt **(PREMIUM)**
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/9045) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.8.
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/9045) in GitLab Premium 12.8.
|
||||
|
||||
You can trigger a pipeline in your project whenever a pipeline finishes for a new
|
||||
tag in a different project:
|
||||
|
|
|
@ -23,6 +23,15 @@ module API
|
|||
helpers ::API::Helpers::PackagesHelpers
|
||||
|
||||
helpers do
|
||||
def path_exists?(path)
|
||||
# return true when FF disabled so that processing the request is not stopped
|
||||
return true unless Feature.enabled?(:check_maven_path_first)
|
||||
return false if path.blank?
|
||||
|
||||
Packages::Maven::Metadatum.with_path(path)
|
||||
.exists?
|
||||
end
|
||||
|
||||
def extract_format(file_name)
|
||||
name, _, format = file_name.rpartition('.')
|
||||
|
||||
|
@ -104,6 +113,9 @@ module API
|
|||
end
|
||||
route_setting :authentication, job_token_allowed: true, deploy_token_allowed: true
|
||||
get 'packages/maven/*path/:file_name', requirements: MAVEN_ENDPOINT_REQUIREMENTS do
|
||||
# return a similar failure to authorize_read_package!(project)
|
||||
forbidden! unless path_exists?(params[:path])
|
||||
|
||||
file_name, format = extract_format(params[:file_name])
|
||||
|
||||
# To avoid name collision we require project path and project package be the same.
|
||||
|
@ -142,6 +154,9 @@ module API
|
|||
end
|
||||
route_setting :authentication, job_token_allowed: true, deploy_token_allowed: true
|
||||
get ':id/-/packages/maven/*path/:file_name', requirements: MAVEN_ENDPOINT_REQUIREMENTS do
|
||||
# return a similar failure to group = find_group(params[:id])
|
||||
not_found!('Group') unless path_exists?(params[:path])
|
||||
|
||||
file_name, format = extract_format(params[:file_name])
|
||||
|
||||
group = find_group(params[:id])
|
||||
|
@ -181,6 +196,9 @@ module API
|
|||
end
|
||||
route_setting :authentication, job_token_allowed: true, deploy_token_allowed: true
|
||||
get ':id/packages/maven/*path/:file_name', requirements: MAVEN_ENDPOINT_REQUIREMENTS do
|
||||
# return a similar failure to user_project
|
||||
not_found!('Project') unless path_exists?(params[:path])
|
||||
|
||||
authorize_read_package!(user_project)
|
||||
|
||||
file_name, format = extract_format(params[:file_name])
|
||||
|
|
|
@ -19640,10 +19640,10 @@ msgstr ""
|
|||
msgid "Members|Are you sure you want to remove \"%{groupName}\"?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Members|Are you sure you want to remove %{usersName} from \"%{source}\""
|
||||
msgid "Members|Are you sure you want to remove %{usersName} from \"%{source}\"?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Members|Are you sure you want to remove this orphaned member from \"%{source}\""
|
||||
msgid "Members|Are you sure you want to remove this orphaned member from \"%{source}\"?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Members|Are you sure you want to revoke the invitation for %{inviteEmail} to join \"%{source}\""
|
||||
|
@ -22011,12 +22011,21 @@ msgstr ""
|
|||
msgid "OnCallSchedules|For this rotation, on-call will be:"
|
||||
msgstr ""
|
||||
|
||||
msgid "OnCallSchedules|On-call schedule %{schedule} in Project %{project}"
|
||||
msgstr ""
|
||||
|
||||
msgid "OnCallSchedules|On-call schedules"
|
||||
msgstr ""
|
||||
|
||||
msgid "OnCallSchedules|Please note, rotations with shifts that are less than four hours are currently not supported in the weekly view."
|
||||
msgstr ""
|
||||
|
||||
msgid "OnCallSchedules|Removing this user may put their on-call team at risk of missing a notification."
|
||||
msgstr ""
|
||||
|
||||
msgid "OnCallSchedules|Removing yourself may put your on-call team at risk of missing a notification."
|
||||
msgstr ""
|
||||
|
||||
msgid "OnCallSchedules|Restrict to time intervals"
|
||||
msgstr ""
|
||||
|
||||
|
@ -22071,12 +22080,18 @@ msgstr ""
|
|||
msgid "OnCallSchedules|Try adding a rotation"
|
||||
msgstr ""
|
||||
|
||||
msgid "OnCallSchedules|User %{name} is currently part of:"
|
||||
msgstr ""
|
||||
|
||||
msgid "OnCallSchedules|View next timeframe"
|
||||
msgstr ""
|
||||
|
||||
msgid "OnCallSchedules|View previous timeframe"
|
||||
msgstr ""
|
||||
|
||||
msgid "OnCallSchedules|You are currently a part of:"
|
||||
msgstr ""
|
||||
|
||||
msgid "OnCallSchedules|Your schedule has been successfully created and all alerts from this project will now be routed to this schedule. Currently, only one schedule can be created per project. More coming soon! To add individual users to this schedule, use the add a rotation button."
|
||||
msgstr ""
|
||||
|
||||
|
@ -37323,15 +37338,15 @@ msgstr ""
|
|||
msgid "mrWidget|Fast-forward merge is not possible. To merge this request, first rebase locally."
|
||||
msgstr ""
|
||||
|
||||
msgid "mrWidget|Fork merge requests do not create merge request pipelines which validate a post merge result"
|
||||
msgstr ""
|
||||
|
||||
msgid "mrWidget|Fork project merge requests do not create merge request pipelines that validate a post merge result unless invoked by a project member."
|
||||
msgstr ""
|
||||
|
||||
msgid "mrWidget|If the %{missingBranchName} branch exists in your local repository, you can merge this merge request manually using the command line"
|
||||
msgstr ""
|
||||
|
||||
msgid "mrWidget|If the last pipeline ran in the fork project, it may be inaccurate. Before merge, we advise running a pipeline in this project."
|
||||
msgstr ""
|
||||
|
||||
msgid "mrWidget|Jump to first unresolved thread"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -55,11 +55,11 @@ module DeprecationToolkitEnv
|
|||
# one by one
|
||||
def self.allowed_kwarg_warning_paths
|
||||
%w[
|
||||
activerecord-6.0.3.4/lib/active_record/migration.rb
|
||||
activesupport-6.0.3.4/lib/active_support/cache.rb
|
||||
activerecord-6.0.3.6/lib/active_record/migration.rb
|
||||
activesupport-6.0.3.6/lib/active_support/cache.rb
|
||||
batch-loader-1.4.0/lib/batch_loader/graphql.rb
|
||||
carrierwave-1.3.1/lib/carrierwave/sanitized_file.rb
|
||||
activerecord-6.0.3.4/lib/active_record/relation.rb
|
||||
activerecord-6.0.3.6/lib/active_record/relation.rb
|
||||
selenium-webdriver-3.142.7/lib/selenium/webdriver/firefox/driver.rb
|
||||
asciidoctor-2.0.12/lib/asciidoctor/extensions.rb
|
||||
]
|
||||
|
|
|
@ -15,7 +15,7 @@ RSpec.describe 'Projects > Settings > For a forked project', :js do
|
|||
end
|
||||
|
||||
shared_examples 'project settings for a forked projects' do
|
||||
it 'allows deleting the link to the forked project' do
|
||||
it 'allows deleting the link to the forked project', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/327817' do
|
||||
visit edit_project_path(forked_project)
|
||||
|
||||
click_button 'Remove fork relationship'
|
||||
|
|
|
@ -183,7 +183,7 @@ RSpec.describe 'Project' do
|
|||
visit edit_project_path(project)
|
||||
end
|
||||
|
||||
it 'removes fork' do
|
||||
it 'removes fork', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/327817' do
|
||||
expect(page).to have_content 'Remove fork relationship'
|
||||
|
||||
remove_with_confirm('Remove fork relationship', project.path)
|
||||
|
|
|
@ -16,8 +16,6 @@ RSpec.describe Projects::IssuesController, '(JavaScript fixtures)', type: :contr
|
|||
end
|
||||
|
||||
before do
|
||||
stub_feature_flags(boards_filtered_search: false)
|
||||
|
||||
project.add_maintainer(user)
|
||||
sign_in(user)
|
||||
end
|
||||
|
|
|
@ -38,6 +38,7 @@ describe('RemoveMemberButton', () => {
|
|||
title: 'Remove member',
|
||||
isAccessRequest: true,
|
||||
isInvite: true,
|
||||
oncallSchedules: { name: 'user', schedules: [] },
|
||||
...propsData,
|
||||
},
|
||||
directives: {
|
||||
|
@ -59,6 +60,7 @@ describe('RemoveMemberButton', () => {
|
|||
'data-message': 'Are you sure you want to remove John Smith?',
|
||||
'data-is-access-request': 'true',
|
||||
'data-is-invite': 'true',
|
||||
'data-oncall-schedules': '{"name":"user","schedules":[]}',
|
||||
'aria-label': 'Remove member',
|
||||
title: 'Remove member',
|
||||
icon: 'remove',
|
||||
|
|
|
@ -40,11 +40,15 @@ describe('UserActionButtons', () => {
|
|||
expect(findRemoveMemberButton().props()).toEqual({
|
||||
memberId: member.id,
|
||||
memberType: 'GroupMember',
|
||||
message: `Are you sure you want to remove ${member.user.name} from "${member.source.fullName}"`,
|
||||
message: `Are you sure you want to remove ${member.user.name} from "${member.source.fullName}"?`,
|
||||
title: 'Remove member',
|
||||
isAccessRequest: false,
|
||||
isInvite: false,
|
||||
icon: 'remove',
|
||||
oncallSchedules: {
|
||||
name: member.user.name,
|
||||
schedules: member.user.oncallSchedules,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -58,7 +62,7 @@ describe('UserActionButtons', () => {
|
|||
});
|
||||
|
||||
expect(findRemoveMemberButton().props('message')).toBe(
|
||||
`Are you sure you want to remove this orphaned member from "${orphanedMember.source.fullName}"`,
|
||||
`Are you sure you want to remove this orphaned member from "${orphanedMember.source.fullName}"?`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import { GlModal, GlForm } from '@gitlab/ui';
|
||||
import { within } from '@testing-library/dom';
|
||||
import { mount, createLocalVue, createWrapper } from '@vue/test-utils';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { nextTick } from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
import LeaveModal from '~/members/components/modals/leave_modal.vue';
|
||||
import { LEAVE_MODAL_ID, MEMBER_TYPES } from '~/members/constants';
|
||||
import OncallSchedulesList from '~/vue_shared/components/oncall_schedules_list.vue';
|
||||
import { member } from '../../mock_data';
|
||||
|
||||
jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
|
||||
|
@ -47,9 +49,9 @@ describe('LeaveModal', () => {
|
|||
});
|
||||
};
|
||||
|
||||
const findModal = () => wrapper.find(GlModal);
|
||||
|
||||
const findForm = () => findModal().find(GlForm);
|
||||
const findModal = () => wrapper.findComponent(GlModal);
|
||||
const findForm = () => findModal().findComponent(GlForm);
|
||||
const findOncallSchedulesList = () => findModal().findComponent(OncallSchedulesList);
|
||||
|
||||
const getByText = (text, options) =>
|
||||
createWrapper(within(findModal().element).getByText(text, options));
|
||||
|
@ -87,6 +89,24 @@ describe('LeaveModal', () => {
|
|||
);
|
||||
});
|
||||
|
||||
describe('On-call schedules list', () => {
|
||||
it("displays oncall schedules list when member's user is part of on-call schedules ", () => {
|
||||
const schedulesList = findOncallSchedulesList();
|
||||
expect(schedulesList.exists()).toBe(true);
|
||||
expect(schedulesList.props()).toMatchObject({
|
||||
isCurrentUser: true,
|
||||
schedules: member.user.oncallSchedules,
|
||||
});
|
||||
});
|
||||
|
||||
it("does NOT display oncall schedules list when member's user is NOT a part of on-call schedules ", () => {
|
||||
const memberWithoutOncallSchedules = cloneDeep(member);
|
||||
delete (memberWithoutOncallSchedules, 'user.oncallSchedules');
|
||||
createComponent({ member: memberWithoutOncallSchedules });
|
||||
expect(findOncallSchedulesList().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('submits the form when "Leave" button is clicked', () => {
|
||||
const submitSpy = jest.spyOn(findForm().element, 'submit');
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ export const member = {
|
|||
avatarUrl: 'https://www.gravatar.com/avatar/4816142ef496f956a277bedf1a40607b?s=80&d=identicon',
|
||||
blocked: false,
|
||||
twoFactorEnabled: false,
|
||||
oncallSchedules: [{ name: 'schedule 1' }],
|
||||
},
|
||||
id: 238,
|
||||
createdAt: '2020-07-17T16:22:46.923Z',
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import { HIGHLIGHT_CLASS_NAME } from '~/vue_shared/components/blob_viewers/constants';
|
||||
import SimpleViewer from '~/vue_shared/components/blob_viewers/simple_viewer.vue';
|
||||
import EditorLite from '~/vue_shared/components/editor_lite.vue';
|
||||
|
@ -8,10 +9,17 @@ describe('Blob Simple Viewer component', () => {
|
|||
const contentMock = `<span id="LC1">First</span>\n<span id="LC2">Second</span>\n<span id="LC3">Third</span>`;
|
||||
const blobHash = 'foo-bar';
|
||||
|
||||
function createComponent(content = contentMock, isRawContent = false) {
|
||||
function createComponent(
|
||||
content = contentMock,
|
||||
isRawContent = false,
|
||||
isRefactorFlagEnabled = false,
|
||||
) {
|
||||
wrapper = shallowMount(SimpleViewer, {
|
||||
provide: {
|
||||
blobHash,
|
||||
glFeatures: {
|
||||
refactorBlobViewer: isRefactorFlagEnabled,
|
||||
},
|
||||
},
|
||||
propsData: {
|
||||
content,
|
||||
|
@ -87,17 +95,31 @@ describe('Blob Simple Viewer component', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('raw content', () => {
|
||||
describe('Vue refactoring to use Source Editor', () => {
|
||||
const findEditorLite = () => wrapper.find(EditorLite);
|
||||
const isRawContent = true;
|
||||
|
||||
it('uses the Editor Lite component in readonly mode when viewing raw content', () => {
|
||||
createComponent('raw content', isRawContent);
|
||||
it.each`
|
||||
doesRender | condition | isRawContent | isRefactorFlagEnabled
|
||||
${'Does not'} | ${'rawContent is not specified'} | ${false} | ${true}
|
||||
${'Does not'} | ${'feature flag is disabled is not specified'} | ${true} | ${false}
|
||||
${'Does not'} | ${'both, the FF and rawContent are not specified'} | ${false} | ${false}
|
||||
${'Does'} | ${'both, the FF and rawContent are specified'} | ${true} | ${true}
|
||||
`(
|
||||
'$doesRender render Editor Lite component in readonly mode when $condition',
|
||||
async ({ isRawContent, isRefactorFlagEnabled } = {}) => {
|
||||
createComponent('raw content', isRawContent, isRefactorFlagEnabled);
|
||||
await waitForPromises();
|
||||
|
||||
expect(findEditorLite().exists()).toBe(true);
|
||||
expect(findEditorLite().props('value')).toBe('raw content');
|
||||
expect(findEditorLite().props('fileName')).toBe('test.js');
|
||||
expect(findEditorLite().props('editorOptions')).toEqual({ readOnly: true });
|
||||
});
|
||||
if (isRawContent && isRefactorFlagEnabled) {
|
||||
expect(findEditorLite().exists()).toBe(true);
|
||||
|
||||
expect(findEditorLite().props('value')).toBe('raw content');
|
||||
expect(findEditorLite().props('fileName')).toBe('test.js');
|
||||
expect(findEditorLite().props('editorOptions')).toEqual({ readOnly: true });
|
||||
} else {
|
||||
expect(findEditorLite().exists()).toBe(false);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { GlLoadingIcon } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
|
||||
import ApplySuggestion from '~/vue_shared/components/markdown/apply_suggestion.vue';
|
||||
import SuggestionDiffHeader from '~/vue_shared/components/markdown/suggestion_diff_header.vue';
|
||||
|
||||
|
@ -22,6 +23,9 @@ describe('Suggestion Diff component', () => {
|
|||
...DEFAULT_PROPS,
|
||||
...props,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: createMockDirective(),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -218,15 +222,23 @@ describe('Suggestion Diff component', () => {
|
|||
});
|
||||
|
||||
describe('tooltip message for apply button', () => {
|
||||
const findTooltip = () => getBinding(findApplyButton().element, 'gl-tooltip');
|
||||
|
||||
it('renders correct tooltip message when button is applicable', () => {
|
||||
createComponent();
|
||||
expect(wrapper.vm.tooltipMessage).toBe('This also resolves this thread');
|
||||
const tooltip = findTooltip();
|
||||
|
||||
expect(tooltip.modifiers.viewport).toBe(true);
|
||||
expect(tooltip.value).toBe('This also resolves this thread');
|
||||
});
|
||||
|
||||
it('renders the inapplicable reason in the tooltip when button is not applicable', () => {
|
||||
const inapplicableReason = 'lorem';
|
||||
createComponent({ canApply: false, inapplicableReason });
|
||||
expect(wrapper.vm.tooltipMessage).toBe(inapplicableReason);
|
||||
const tooltip = findTooltip();
|
||||
|
||||
expect(tooltip.modifiers.viewport).toBe(true);
|
||||
expect(tooltip.value).toBe(inapplicableReason);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,13 +1,25 @@
|
|||
import { GlModal } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import OncallSchedulesList from '~/vue_shared/components/oncall_schedules_list.vue';
|
||||
import RemoveMemberModal from '~/vue_shared/components/remove_member_modal.vue';
|
||||
|
||||
const mockSchedules = JSON.stringify({
|
||||
schedules: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Schedule 1',
|
||||
},
|
||||
],
|
||||
name: 'User1',
|
||||
});
|
||||
|
||||
describe('RemoveMemberModal', () => {
|
||||
const memberPath = '/gitlab-org/gitlab-test/-/project_members/90';
|
||||
let wrapper;
|
||||
|
||||
const findForm = () => wrapper.find({ ref: 'form' });
|
||||
const findGlModal = () => wrapper.find(GlModal);
|
||||
const findGlModal = () => wrapper.findComponent(GlModal);
|
||||
const findOnCallSchedulesList = () => wrapper.findComponent(OncallSchedulesList);
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
|
@ -15,11 +27,11 @@ describe('RemoveMemberModal', () => {
|
|||
});
|
||||
|
||||
describe.each`
|
||||
state | memberType | isAccessRequest | isInvite | actionText | removeSubMembershipsCheckboxExpected | unassignIssuablesCheckboxExpected | message
|
||||
${'removing a group member'} | ${'GroupMember'} | ${'false'} | ${'false'} | ${'Remove member'} | ${true} | ${true} | ${'Are you sure you want to remove Jane Doe from the Gitlab Org / Gitlab Test project?'}
|
||||
${'removing a project member'} | ${'ProjectMember'} | ${'false'} | ${'false'} | ${'Remove member'} | ${false} | ${true} | ${'Are you sure you want to remove Jane Doe from the Gitlab Org / Gitlab Test project?'}
|
||||
${'denying an access request'} | ${'ProjectMember'} | ${'true'} | ${'false'} | ${'Deny access request'} | ${false} | ${false} | ${"Are you sure you want to deny Jane Doe's request to join the Gitlab Org / Gitlab Test project?"}
|
||||
${'revoking invite'} | ${'ProjectMember'} | ${'false'} | ${'true'} | ${'Revoke invite'} | ${false} | ${false} | ${'Are you sure you want to revoke the invitation for foo@bar.com to join the Gitlab Org / Gitlab Test project?'}
|
||||
state | memberType | isAccessRequest | isInvite | actionText | removeSubMembershipsCheckboxExpected | unassignIssuablesCheckboxExpected | message | onCallSchedules
|
||||
${'removing a group member'} | ${'GroupMember'} | ${false} | ${'false'} | ${'Remove member'} | ${true} | ${true} | ${'Are you sure you want to remove Jane Doe from the Gitlab Org / Gitlab Test project?'} | ${`{}`}
|
||||
${'removing a project member'} | ${'ProjectMember'} | ${false} | ${'false'} | ${'Remove member'} | ${false} | ${true} | ${'Are you sure you want to remove Jane Doe from the Gitlab Org / Gitlab Test project?'} | ${mockSchedules}
|
||||
${'denying an access request'} | ${'ProjectMember'} | ${true} | ${'false'} | ${'Deny access request'} | ${false} | ${false} | ${"Are you sure you want to deny Jane Doe's request to join the Gitlab Org / Gitlab Test project?"} | ${`{}`}
|
||||
${'revoking invite'} | ${'ProjectMember'} | ${false} | ${'true'} | ${'Revoke invite'} | ${false} | ${false} | ${'Are you sure you want to revoke the invitation for foo@bar.com to join the Gitlab Org / Gitlab Test project?'} | ${mockSchedules}
|
||||
`(
|
||||
'when $state',
|
||||
({
|
||||
|
@ -30,6 +42,7 @@ describe('RemoveMemberModal', () => {
|
|||
message,
|
||||
removeSubMembershipsCheckboxExpected,
|
||||
unassignIssuablesCheckboxExpected,
|
||||
onCallSchedules,
|
||||
}) => {
|
||||
beforeEach(() => {
|
||||
wrapper = shallowMount(RemoveMemberModal, {
|
||||
|
@ -41,12 +54,16 @@ describe('RemoveMemberModal', () => {
|
|||
message,
|
||||
memberPath,
|
||||
memberType,
|
||||
onCallSchedules,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const parsedSchedules = JSON.parse(onCallSchedules);
|
||||
const isPartOfOncallSchedules = Boolean(isAccessRequest && parsedSchedules.schedules?.length);
|
||||
|
||||
it(`has the title ${actionText}`, () => {
|
||||
expect(findGlModal().attributes('title')).toBe(actionText);
|
||||
});
|
||||
|
@ -75,6 +92,10 @@ describe('RemoveMemberModal', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it(`shows ${isPartOfOncallSchedules ? 'all' : 'no'} related on-call schedules`, () => {
|
||||
expect(findOnCallSchedulesList().exists()).toBe(isPartOfOncallSchedules);
|
||||
});
|
||||
|
||||
it('submits the form when the modal is submitted', () => {
|
||||
const spy = jest.spyOn(findForm().element, 'submit');
|
||||
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
import { GlLink, GlSprintf } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
|
||||
import OncallSchedulesList from '~/vue_shared/components/oncall_schedules_list.vue';
|
||||
|
||||
const mockSchedules = [
|
||||
{
|
||||
name: 'Schedule 1',
|
||||
scheduleUrl: 'http://gitlab.com/gitlab-org/gitlab-shell/-/oncall_schedules',
|
||||
projectName: 'Shell',
|
||||
projectUrl: 'http://gitlab.com/gitlab-org/gitlab-shell/',
|
||||
},
|
||||
{
|
||||
name: 'Schedule 2',
|
||||
scheduleUrl: 'http://gitlab.com/gitlab-org/gitlab-ui/-/oncall_schedules',
|
||||
projectName: 'UI',
|
||||
projectUrl: 'http://gitlab.com/gitlab-org/gitlab-ui/',
|
||||
},
|
||||
];
|
||||
|
||||
const userName = 'User 1';
|
||||
|
||||
describe('On-call schedules list', () => {
|
||||
let wrapper;
|
||||
|
||||
function createComponent(props) {
|
||||
wrapper = extendedWrapper(
|
||||
shallowMount(OncallSchedulesList, {
|
||||
propsData: {
|
||||
schedules: mockSchedules,
|
||||
userName,
|
||||
...props,
|
||||
},
|
||||
stubs: {
|
||||
GlSprintf,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
const findLinks = () => wrapper.findAllComponents(GlLink);
|
||||
const findTitle = () => wrapper.findByTestId('title');
|
||||
const findFooter = () => wrapper.findByTestId('footer');
|
||||
const findSchedules = () => wrapper.findByTestId('schedules-list');
|
||||
|
||||
describe.each`
|
||||
isCurrentUser | titleText | footerText
|
||||
${true} | ${'You are currently a part of:'} | ${'Removing yourself may put your on-call team at risk of missing a notification.'}
|
||||
${false} | ${`User ${userName} is currently part of:`} | ${'Removing this user may put their on-call team at risk of missing a notification.'}
|
||||
`('when current user ', ({ isCurrentUser, titleText, footerText }) => {
|
||||
it(`${isCurrentUser ? 'is' : 'is not'} a part of on-call schedule`, async () => {
|
||||
createComponent({
|
||||
isCurrentUser,
|
||||
});
|
||||
|
||||
expect(findTitle().text()).toBe(titleText);
|
||||
expect(findFooter().text()).toBe(footerText);
|
||||
});
|
||||
});
|
||||
|
||||
describe.each(mockSchedules)(
|
||||
'renders each on-call schedule data',
|
||||
({ name, scheduleUrl, projectName, projectUrl }) => {
|
||||
beforeEach(() => {
|
||||
createComponent({ schedules: [{ name, scheduleUrl, projectName, projectUrl }] });
|
||||
});
|
||||
|
||||
it(`renders schedule ${name}'s name and link`, () => {
|
||||
const msg = findSchedules().text();
|
||||
|
||||
expect(msg).toContain(`On-call schedule ${name}`);
|
||||
expect(findLinks().at(0).attributes('href')).toBe(scheduleUrl);
|
||||
});
|
||||
|
||||
it(`renders project ${projectName}'s name and link`, () => {
|
||||
const msg = findSchedules().text();
|
||||
|
||||
expect(msg).toContain(`in Project ${projectName}`);
|
||||
expect(findLinks().at(1).attributes('href')).toBe(projectUrl);
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
|
@ -5,7 +5,7 @@ require 'spec_helper'
|
|||
RSpec.describe Gitlab::Pagination::OffsetHeaderBuilder, type: :controller do
|
||||
controller(ActionController::Base) do
|
||||
def index
|
||||
relation = Project.where(archived: params[:archived]).page(params[:page]).per(1)
|
||||
relation = Project.where(archived: params[:archived]).page(params[:page]).order(:id).per(1)
|
||||
|
||||
params_for_pagination = { archived: params[:archived], page: params[:page] }
|
||||
|
||||
|
@ -22,7 +22,7 @@ RSpec.describe Gitlab::Pagination::OffsetHeaderBuilder, type: :controller do
|
|||
end
|
||||
end
|
||||
|
||||
let_it_be(:projects) { create_list(:project, 2, archived: true) }
|
||||
let_it_be(:projects) { create_list(:project, 2, archived: true).sort_by(&:id) }
|
||||
|
||||
describe 'pagination' do
|
||||
it 'returns correct result for the first page' do
|
||||
|
|
|
@ -681,39 +681,178 @@ RSpec.describe Group do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#last_blocked_owner?' do
|
||||
let(:blocked_user) { create(:user, :blocked) }
|
||||
describe '#member_last_blocked_owner?' do
|
||||
let_it_be(:blocked_user) { create(:user, :blocked) }
|
||||
|
||||
let(:member) { blocked_user.group_members.last }
|
||||
|
||||
before do
|
||||
group.add_user(blocked_user, GroupMember::OWNER)
|
||||
end
|
||||
|
||||
it { expect(group.last_blocked_owner?(blocked_user)).to be_truthy }
|
||||
|
||||
context 'with another active owner' do
|
||||
context 'when last_blocked_owner is set' do
|
||||
before do
|
||||
group.add_user(create(:user), GroupMember::OWNER)
|
||||
expect(group).not_to receive(:members_with_parents)
|
||||
end
|
||||
|
||||
it { expect(group.last_blocked_owner?(blocked_user)).to be_falsy }
|
||||
it 'returns true' do
|
||||
member.last_blocked_owner = true
|
||||
|
||||
expect(group.member_last_blocked_owner?(member)).to be(true)
|
||||
end
|
||||
|
||||
it 'returns false' do
|
||||
member.last_blocked_owner = false
|
||||
|
||||
expect(group.member_last_blocked_owner?(member)).to be(false)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with 2 blocked owners' do
|
||||
before do
|
||||
group.add_user(create(:user, :blocked), GroupMember::OWNER)
|
||||
context 'when last_blocked_owner is not set' do
|
||||
it { expect(group.member_last_blocked_owner?(member)).to be(true) }
|
||||
|
||||
context 'with another active owner' do
|
||||
before do
|
||||
group.add_user(create(:user), GroupMember::OWNER)
|
||||
end
|
||||
|
||||
it { expect(group.member_last_blocked_owner?(member)).to be(false) }
|
||||
end
|
||||
|
||||
it { expect(group.last_blocked_owner?(blocked_user)).to be_falsy }
|
||||
context 'with 2 blocked owners' do
|
||||
before do
|
||||
group.add_user(create(:user, :blocked), GroupMember::OWNER)
|
||||
end
|
||||
|
||||
it { expect(group.member_last_blocked_owner?(member)).to be(false) }
|
||||
end
|
||||
|
||||
context 'with owners from a parent' do
|
||||
before do
|
||||
parent_group = create(:group)
|
||||
create(:group_member, :owner, group: parent_group)
|
||||
group.update(parent: parent_group)
|
||||
end
|
||||
|
||||
it { expect(group.member_last_blocked_owner?(member)).to be(false) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when analyzing blocked owners' do
|
||||
let_it_be(:blocked_user) { create(:user, :blocked) }
|
||||
|
||||
describe '#single_blocked_owner?' do
|
||||
context 'when there is only one blocked owner' do
|
||||
before do
|
||||
group.add_user(blocked_user, GroupMember::OWNER)
|
||||
end
|
||||
|
||||
it 'returns true' do
|
||||
expect(group.single_blocked_owner?).to eq(true)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there are multiple blocked owners' do
|
||||
let_it_be(:blocked_user_2) { create(:user, :blocked) }
|
||||
|
||||
before do
|
||||
group.add_user(blocked_user, GroupMember::OWNER)
|
||||
group.add_user(blocked_user_2, GroupMember::OWNER)
|
||||
end
|
||||
|
||||
it 'returns true' do
|
||||
expect(group.single_blocked_owner?).to eq(false)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there are no blocked owners' do
|
||||
it 'returns false' do
|
||||
expect(group.single_blocked_owner?).to eq(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with owners from a parent' do
|
||||
describe '#blocked_owners' do
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
before do
|
||||
parent_group = create(:group)
|
||||
create(:group_member, :owner, group: parent_group)
|
||||
group.update(parent: parent_group)
|
||||
group.add_user(blocked_user, GroupMember::OWNER)
|
||||
group.add_user(user, GroupMember::OWNER)
|
||||
end
|
||||
|
||||
it { expect(group.last_blocked_owner?(blocked_user)).to be_falsy }
|
||||
it 'has only blocked owners' do
|
||||
expect(group.blocked_owners.map(&:user)).to match([blocked_user])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#single_owner?' do
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
context 'when there is only one owner' do
|
||||
before do
|
||||
group.add_user(user, GroupMember::OWNER)
|
||||
end
|
||||
|
||||
it 'returns true' do
|
||||
expect(group.single_owner?).to eq(true)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there are multiple owners' do
|
||||
let_it_be(:user_2) { create(:user) }
|
||||
|
||||
before do
|
||||
group.add_user(user, GroupMember::OWNER)
|
||||
group.add_user(user_2, GroupMember::OWNER)
|
||||
end
|
||||
|
||||
it 'returns true' do
|
||||
expect(group.single_owner?).to eq(false)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there are no owners' do
|
||||
it 'returns false' do
|
||||
expect(group.single_owner?).to eq(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#member_last_owner?' do
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
let(:member) { group.members.last }
|
||||
|
||||
before do
|
||||
group.add_user(user, GroupMember::OWNER)
|
||||
end
|
||||
|
||||
context 'when last_owner is set' do
|
||||
before do
|
||||
expect(group).not_to receive(:last_owner?)
|
||||
end
|
||||
|
||||
it 'returns true' do
|
||||
member.last_owner = true
|
||||
|
||||
expect(group.member_last_owner?(member)).to be(true)
|
||||
end
|
||||
|
||||
it 'returns false' do
|
||||
member.last_owner = false
|
||||
|
||||
expect(group.member_last_owner?(member)).to be(false)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when last_owner is not set' do
|
||||
it 'returns true' do
|
||||
expect(group).to receive(:last_owner?).and_call_original
|
||||
|
||||
expect(group.member_last_owner?(member)).to be(true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -0,0 +1,98 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Members::LastGroupOwnerAssigner do
|
||||
describe "#execute" do
|
||||
let_it_be(:user, reload: true) { create(:user) }
|
||||
let_it_be(:group) { create(:group) }
|
||||
|
||||
let(:group_member) { user.members.last }
|
||||
|
||||
subject(:assigner) { described_class.new(group, [group_member]) }
|
||||
|
||||
before do
|
||||
group.add_owner(user)
|
||||
end
|
||||
|
||||
it "avoids extra database queries utilizing memoization", :aggregate_failures do
|
||||
control = ActiveRecord::QueryRecorder.new { assigner.execute }
|
||||
count_queries = control.occurrences_by_line_method.first[1][:occurrences].find_all { |i| i.include?('SELECT COUNT') }
|
||||
|
||||
expect(control.count).to be <= 5
|
||||
expect(count_queries.count).to eq(0)
|
||||
end
|
||||
|
||||
context "when there are unblocked owners" do
|
||||
context "with one unblocked owner" do
|
||||
specify do
|
||||
expect { assigner.execute }.to change(group_member, :last_owner)
|
||||
.from(nil).to(true)
|
||||
.and change(group_member, :last_blocked_owner)
|
||||
.from(nil).to(false)
|
||||
end
|
||||
end
|
||||
|
||||
context "with multiple unblocked owners" do
|
||||
let_it_be(:unblocked_owner_member) { create(:group_member, :owner, source: group) }
|
||||
|
||||
specify do
|
||||
expect { assigner.execute }.to change(group_member, :last_owner)
|
||||
.from(nil).to(false)
|
||||
.and change(group_member, :last_blocked_owner)
|
||||
.from(nil).to(false)
|
||||
end
|
||||
|
||||
it "has many members passed" do
|
||||
assigner = described_class.new(group, [unblocked_owner_member, group_member])
|
||||
|
||||
expect { assigner.execute }.to change(group_member, :last_owner)
|
||||
.from(nil).to(false)
|
||||
.and change(group_member, :last_blocked_owner)
|
||||
.from(nil).to(false)
|
||||
.and change(unblocked_owner_member, :last_owner)
|
||||
.from(nil).to(false)
|
||||
.and change(unblocked_owner_member, :last_blocked_owner)
|
||||
.from(nil).to(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when there are blocked owners" do
|
||||
before do
|
||||
user.block!
|
||||
end
|
||||
|
||||
context "with one blocked owner" do
|
||||
specify do
|
||||
expect { assigner.execute }.to change(group_member, :last_owner)
|
||||
.from(nil).to(false)
|
||||
.and change(group_member, :last_blocked_owner)
|
||||
.from(nil).to(true)
|
||||
end
|
||||
end
|
||||
|
||||
context "with multiple unblocked owners" do
|
||||
specify do
|
||||
create_list(:group_member, 2, :owner, source: group)
|
||||
|
||||
expect { assigner.execute }.to change(group_member, :last_owner)
|
||||
.from(nil).to(false)
|
||||
.and change(group_member, :last_blocked_owner)
|
||||
.from(nil).to(false)
|
||||
end
|
||||
end
|
||||
|
||||
context "with multiple blocked owners" do
|
||||
specify do
|
||||
create(:group_member, :owner, :blocked, source: group)
|
||||
|
||||
expect { assigner.execute }.to change(group_member, :last_owner)
|
||||
.from(nil).to(false)
|
||||
.and change(group_member, :last_blocked_owner)
|
||||
.from(nil).to(false)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -54,7 +54,7 @@ RSpec.describe Packages::Maven::Metadatum, type: :model do
|
|||
let_it_be(:metadatum3) { create(:maven_metadatum, package: package) }
|
||||
let_it_be(:metadatum4) { create(:maven_metadatum, package: package) }
|
||||
|
||||
subject { Packages::Maven::Metadatum.for_package_ids(package.id).order_created }
|
||||
subject { described_class.for_package_ids(package.id).order_created }
|
||||
|
||||
it { is_expected.to eq([metadatum1, metadatum2, metadatum3, metadatum4]) }
|
||||
end
|
||||
|
@ -64,10 +64,20 @@ RSpec.describe Packages::Maven::Metadatum, type: :model do
|
|||
let_it_be(:metadatum2) { create(:maven_metadatum, package: package, app_name: 'two') }
|
||||
let_it_be(:metadatum3) { create(:maven_metadatum, package: package, app_name: 'three') }
|
||||
|
||||
subject { Packages::Maven::Metadatum.for_package_ids(package.id).pluck_app_name }
|
||||
subject { described_class.for_package_ids(package.id).pluck_app_name }
|
||||
|
||||
it { is_expected.to match_array([metadatum1, metadatum2, metadatum3].map(&:app_name)) }
|
||||
end
|
||||
|
||||
describe '.with_path' do
|
||||
let_it_be(:metadatum1) { create(:maven_metadatum, package: package, path: 'one') }
|
||||
let_it_be(:metadatum2) { create(:maven_metadatum, package: package, path: 'two') }
|
||||
let_it_be(:metadatum3) { create(:maven_metadatum, package: package, path: 'three') }
|
||||
|
||||
subject { described_class.with_path('two') }
|
||||
|
||||
it { is_expected.to match_array([metadatum2]) }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -47,7 +47,21 @@ RSpec.describe API::MavenPackages do
|
|||
end
|
||||
end
|
||||
|
||||
shared_examples 'processing HEAD requests' do
|
||||
shared_examples 'rejecting the request for non existing maven path' do |expected_status: :not_found|
|
||||
before do
|
||||
if Feature.enabled?(:check_maven_path_first)
|
||||
expect(::Packages::Maven::PackageFinder).not_to receive(:new)
|
||||
end
|
||||
end
|
||||
|
||||
it 'rejects the request' do
|
||||
subject
|
||||
|
||||
expect(response).to have_gitlab_http_status(expected_status)
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'processing HEAD requests' do |instance_level: false|
|
||||
subject { head api(url) }
|
||||
|
||||
before do
|
||||
|
@ -92,6 +106,12 @@ RSpec.describe API::MavenPackages do
|
|||
|
||||
subject
|
||||
end
|
||||
|
||||
context 'with a non existing maven path' do
|
||||
let(:path) { 'foo/bar/1.2.3' }
|
||||
|
||||
it_behaves_like 'rejecting the request for non existing maven path', expected_status: instance_level ? :forbidden : :not_found
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -99,9 +119,8 @@ RSpec.describe API::MavenPackages do
|
|||
context 'successful download' do
|
||||
subject do
|
||||
download_file(
|
||||
package_file.file_name,
|
||||
{},
|
||||
Gitlab::Auth::AuthFinders::DEPLOY_TOKEN_HEADER => deploy_token.token
|
||||
file_name: package_file.file_name,
|
||||
request_headers: { Gitlab::Auth::AuthFinders::DEPLOY_TOKEN_HEADER => deploy_token.token }
|
||||
)
|
||||
end
|
||||
|
||||
|
@ -126,7 +145,7 @@ RSpec.describe API::MavenPackages do
|
|||
shared_examples 'downloads with a job token' do
|
||||
context 'with a running job' do
|
||||
it 'allows download with job token' do
|
||||
download_file(package_file.file_name, job_token: job.token)
|
||||
download_file(file_name: package_file.file_name, params: { job_token: job.token })
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response.media_type).to eq('application/octet-stream')
|
||||
|
@ -139,7 +158,7 @@ RSpec.describe API::MavenPackages do
|
|||
end
|
||||
|
||||
it 'returns unauthorized error' do
|
||||
download_file(package_file.file_name, job_token: job.token)
|
||||
download_file(file_name: package_file.file_name, params: { job_token: job.token })
|
||||
|
||||
expect(response).to have_gitlab_http_status(:unauthorized)
|
||||
end
|
||||
|
@ -149,7 +168,7 @@ RSpec.describe API::MavenPackages do
|
|||
describe 'GET /api/v4/packages/maven/*path/:file_name' do
|
||||
shared_examples 'handling all conditions' do
|
||||
context 'a public project' do
|
||||
subject { download_file(package_file.file_name) }
|
||||
subject { download_file(file_name: package_file.file_name) }
|
||||
|
||||
it_behaves_like 'tracking the file download event'
|
||||
|
||||
|
@ -161,12 +180,18 @@ RSpec.describe API::MavenPackages do
|
|||
end
|
||||
|
||||
it 'returns sha1 of the file' do
|
||||
download_file(package_file.file_name + '.sha1')
|
||||
download_file(file_name: package_file.file_name + '.sha1')
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response.media_type).to eq('text/plain')
|
||||
expect(response.body).to eq(package_file.file_sha1)
|
||||
end
|
||||
|
||||
context 'with a non existing maven path' do
|
||||
subject { download_file(file_name: package_file.file_name, path: 'foo/bar/1.2.3') }
|
||||
|
||||
it_behaves_like 'rejecting the request for non existing maven path', expected_status: :forbidden
|
||||
end
|
||||
end
|
||||
|
||||
context 'internal project' do
|
||||
|
@ -175,7 +200,7 @@ RSpec.describe API::MavenPackages do
|
|||
project.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
|
||||
end
|
||||
|
||||
subject { download_file_with_token(package_file.file_name) }
|
||||
subject { download_file_with_token(file_name: package_file.file_name) }
|
||||
|
||||
it_behaves_like 'tracking the file download event'
|
||||
|
||||
|
@ -187,7 +212,7 @@ RSpec.describe API::MavenPackages do
|
|||
end
|
||||
|
||||
it 'denies download when no private token' do
|
||||
download_file(package_file.file_name)
|
||||
download_file(file_name: package_file.file_name)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:forbidden)
|
||||
end
|
||||
|
@ -195,10 +220,16 @@ RSpec.describe API::MavenPackages do
|
|||
it_behaves_like 'downloads with a job token'
|
||||
|
||||
it_behaves_like 'downloads with a deploy token'
|
||||
|
||||
context 'with a non existing maven path' do
|
||||
subject { download_file_with_token(file_name: package_file.file_name, path: 'foo/bar/1.2.3') }
|
||||
|
||||
it_behaves_like 'rejecting the request for non existing maven path', expected_status: :forbidden
|
||||
end
|
||||
end
|
||||
|
||||
context 'private project' do
|
||||
subject { download_file_with_token(package_file.file_name) }
|
||||
subject { download_file_with_token(file_name: package_file.file_name) }
|
||||
|
||||
before do
|
||||
project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
|
||||
|
@ -222,7 +253,7 @@ RSpec.describe API::MavenPackages do
|
|||
end
|
||||
|
||||
it 'denies download when no private token' do
|
||||
download_file(package_file.file_name)
|
||||
download_file(file_name: package_file.file_name)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:forbidden)
|
||||
end
|
||||
|
@ -241,13 +272,18 @@ RSpec.describe API::MavenPackages do
|
|||
unauthorized_deploy_token.update!(id: another_user.id)
|
||||
|
||||
download_file(
|
||||
package_file.file_name,
|
||||
{},
|
||||
Gitlab::Auth::AuthFinders::DEPLOY_TOKEN_HEADER => unauthorized_deploy_token.token
|
||||
file_name: package_file.file_name,
|
||||
request_headers: { Gitlab::Auth::AuthFinders::DEPLOY_TOKEN_HEADER => unauthorized_deploy_token.token }
|
||||
)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:forbidden)
|
||||
end
|
||||
|
||||
context 'with a non existing maven path' do
|
||||
subject { download_file_with_token(file_name: package_file.file_name, path: 'foo/bar/1.2.3') }
|
||||
|
||||
it_behaves_like 'rejecting the request for non existing maven path', expected_status: :forbidden
|
||||
end
|
||||
end
|
||||
|
||||
context 'project name is different from a package name' do
|
||||
|
@ -256,7 +292,7 @@ RSpec.describe API::MavenPackages do
|
|||
end
|
||||
|
||||
it 'rejects request' do
|
||||
download_file(package_file.file_name)
|
||||
download_file(file_name: package_file.file_name)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:forbidden)
|
||||
end
|
||||
|
@ -279,26 +315,43 @@ RSpec.describe API::MavenPackages do
|
|||
it_behaves_like 'handling all conditions'
|
||||
end
|
||||
|
||||
def download_file(file_name, params = {}, request_headers = headers)
|
||||
get api("/packages/maven/#{maven_metadatum.path}/#{file_name}"), params: params, headers: request_headers
|
||||
context 'with check_maven_path_first enabled' do
|
||||
before do
|
||||
stub_feature_flags(check_maven_path_first: true)
|
||||
end
|
||||
|
||||
it_behaves_like 'handling all conditions'
|
||||
end
|
||||
|
||||
def download_file_with_token(file_name, params = {}, request_headers = headers_with_token)
|
||||
download_file(file_name, params, request_headers)
|
||||
context 'with check_maven_path_first disabled' do
|
||||
before do
|
||||
stub_feature_flags(check_maven_path_first: false)
|
||||
end
|
||||
|
||||
it_behaves_like 'handling all conditions'
|
||||
end
|
||||
|
||||
def download_file(file_name:, params: {}, request_headers: headers, path: maven_metadatum.path)
|
||||
get api("/packages/maven/#{path}/#{file_name}"), params: params, headers: request_headers
|
||||
end
|
||||
|
||||
def download_file_with_token(file_name:, params: {}, request_headers: headers_with_token, path: maven_metadatum.path)
|
||||
download_file(file_name: file_name, params: params, request_headers: request_headers, path: path)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'HEAD /api/v4/packages/maven/*path/:file_name' do
|
||||
let(:url) { "/packages/maven/#{package.maven_metadatum.path}/#{package_file.file_name}" }
|
||||
let(:path) { package.maven_metadatum.path }
|
||||
let(:url) { "/packages/maven/#{path}/#{package_file.file_name}" }
|
||||
|
||||
it_behaves_like 'processing HEAD requests'
|
||||
it_behaves_like 'processing HEAD requests', instance_level: true
|
||||
|
||||
context 'with maven_packages_group_level_improvements enabled' do
|
||||
before do
|
||||
stub_feature_flags(maven_packages_group_level_improvements: true)
|
||||
end
|
||||
|
||||
it_behaves_like 'processing HEAD requests'
|
||||
it_behaves_like 'processing HEAD requests', instance_level: true
|
||||
end
|
||||
|
||||
context 'with maven_packages_group_level_improvements disabled' do
|
||||
|
@ -306,7 +359,23 @@ RSpec.describe API::MavenPackages do
|
|||
stub_feature_flags(maven_packages_group_level_improvements: false)
|
||||
end
|
||||
|
||||
it_behaves_like 'processing HEAD requests'
|
||||
it_behaves_like 'processing HEAD requests', instance_level: true
|
||||
end
|
||||
|
||||
context 'with check_maven_path_first enabled' do
|
||||
before do
|
||||
stub_feature_flags(check_maven_path_first: true)
|
||||
end
|
||||
|
||||
it_behaves_like 'processing HEAD requests', instance_level: true
|
||||
end
|
||||
|
||||
context 'with check_maven_path_first disabled' do
|
||||
before do
|
||||
stub_feature_flags(check_maven_path_first: false)
|
||||
end
|
||||
|
||||
it_behaves_like 'processing HEAD requests', instance_level: true
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -318,7 +387,7 @@ RSpec.describe API::MavenPackages do
|
|||
|
||||
shared_examples 'handling all conditions' do
|
||||
context 'a public project' do
|
||||
subject { download_file(package_file.file_name) }
|
||||
subject { download_file(file_name: package_file.file_name) }
|
||||
|
||||
it_behaves_like 'tracking the file download event'
|
||||
|
||||
|
@ -330,12 +399,18 @@ RSpec.describe API::MavenPackages do
|
|||
end
|
||||
|
||||
it 'returns sha1 of the file' do
|
||||
download_file(package_file.file_name + '.sha1')
|
||||
download_file(file_name: package_file.file_name + '.sha1')
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response.media_type).to eq('text/plain')
|
||||
expect(response.body).to eq(package_file.file_sha1)
|
||||
end
|
||||
|
||||
context 'with a non existing maven path' do
|
||||
subject { download_file(file_name: package_file.file_name, path: 'foo/bar/1.2.3') }
|
||||
|
||||
it_behaves_like 'rejecting the request for non existing maven path'
|
||||
end
|
||||
end
|
||||
|
||||
context 'internal project' do
|
||||
|
@ -344,7 +419,7 @@ RSpec.describe API::MavenPackages do
|
|||
project.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
|
||||
end
|
||||
|
||||
subject { download_file_with_token(package_file.file_name) }
|
||||
subject { download_file_with_token(file_name: package_file.file_name) }
|
||||
|
||||
it_behaves_like 'tracking the file download event'
|
||||
|
||||
|
@ -356,7 +431,7 @@ RSpec.describe API::MavenPackages do
|
|||
end
|
||||
|
||||
it 'denies download when no private token' do
|
||||
download_file(package_file.file_name)
|
||||
download_file(file_name: package_file.file_name)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
|
@ -364,6 +439,12 @@ RSpec.describe API::MavenPackages do
|
|||
it_behaves_like 'downloads with a job token'
|
||||
|
||||
it_behaves_like 'downloads with a deploy token'
|
||||
|
||||
context 'with a non existing maven path' do
|
||||
subject { download_file_with_token(file_name: package_file.file_name, path: 'foo/bar/1.2.3') }
|
||||
|
||||
it_behaves_like 'rejecting the request for non existing maven path'
|
||||
end
|
||||
end
|
||||
|
||||
context 'private project' do
|
||||
|
@ -371,7 +452,7 @@ RSpec.describe API::MavenPackages do
|
|||
project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
|
||||
end
|
||||
|
||||
subject { download_file_with_token(package_file.file_name) }
|
||||
subject { download_file_with_token(file_name: package_file.file_name) }
|
||||
|
||||
it_behaves_like 'tracking the file download event'
|
||||
|
||||
|
@ -392,7 +473,7 @@ RSpec.describe API::MavenPackages do
|
|||
end
|
||||
|
||||
it 'denies download when no private token' do
|
||||
download_file(package_file.file_name)
|
||||
download_file(file_name: package_file.file_name)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
|
@ -401,8 +482,14 @@ RSpec.describe API::MavenPackages do
|
|||
|
||||
it_behaves_like 'downloads with a deploy token'
|
||||
|
||||
context 'with a non existing maven path' do
|
||||
subject { download_file_with_token(file_name: package_file.file_name, path: 'foo/bar/1.2.3') }
|
||||
|
||||
it_behaves_like 'rejecting the request for non existing maven path'
|
||||
end
|
||||
|
||||
context 'with group deploy token' do
|
||||
subject { download_file_with_token(package_file.file_name, {}, group_deploy_token_headers) }
|
||||
subject { download_file_with_token(file_name: package_file.file_name, request_headers: group_deploy_token_headers) }
|
||||
|
||||
it 'returns the file' do
|
||||
subject
|
||||
|
@ -419,13 +506,19 @@ RSpec.describe API::MavenPackages do
|
|||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response.media_type).to eq('application/octet-stream')
|
||||
end
|
||||
|
||||
context 'with a non existing maven path' do
|
||||
subject { download_file_with_token(file_name: package_file.file_name, path: 'foo/bar/1.2.3', request_headers: group_deploy_token_headers) }
|
||||
|
||||
it_behaves_like 'rejecting the request for non existing maven path'
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a reporter from a subgroup accessing the root group' do
|
||||
let_it_be(:root_group) { create(:group, :private) }
|
||||
let_it_be(:group) { create(:group, :private, parent: root_group) }
|
||||
|
||||
subject { download_file_with_token(package_file.file_name, {}, headers_with_token, root_group.id) }
|
||||
subject { download_file_with_token(file_name: package_file.file_name, request_headers: headers_with_token, group_id: root_group.id) }
|
||||
|
||||
before do
|
||||
project.update!(namespace: group)
|
||||
|
@ -438,6 +531,12 @@ RSpec.describe API::MavenPackages do
|
|||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response.media_type).to eq('application/octet-stream')
|
||||
end
|
||||
|
||||
context 'with a non existing maven path' do
|
||||
subject { download_file_with_token(file_name: package_file.file_name, path: 'foo/bar/1.2.3', request_headers: headers_with_token, group_id: root_group.id) }
|
||||
|
||||
it_behaves_like 'rejecting the request for non existing maven path'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -457,7 +556,7 @@ RSpec.describe API::MavenPackages do
|
|||
|
||||
let(:maven_metadatum) { package3.maven_metadatum }
|
||||
|
||||
subject { download_file_with_token(package_file3.file_name) }
|
||||
subject { download_file_with_token(file_name: package_file3.file_name) }
|
||||
|
||||
before do
|
||||
sub_group1.add_developer(user)
|
||||
|
@ -511,17 +610,34 @@ RSpec.describe API::MavenPackages do
|
|||
it_behaves_like 'handling all conditions'
|
||||
end
|
||||
|
||||
def download_file(file_name, params = {}, request_headers = headers, group_id = group.id)
|
||||
get api("/groups/#{group_id}/-/packages/maven/#{maven_metadatum.path}/#{file_name}"), params: params, headers: request_headers
|
||||
context 'with check_maven_path_first enabled' do
|
||||
before do
|
||||
stub_feature_flags(check_maven_path_first: true)
|
||||
end
|
||||
|
||||
it_behaves_like 'handling all conditions'
|
||||
end
|
||||
|
||||
def download_file_with_token(file_name, params = {}, request_headers = headers_with_token, group_id = group.id)
|
||||
download_file(file_name, params, request_headers, group_id)
|
||||
context 'with check_maven_path_first disabled' do
|
||||
before do
|
||||
stub_feature_flags(check_maven_path_first: false)
|
||||
end
|
||||
|
||||
it_behaves_like 'handling all conditions'
|
||||
end
|
||||
|
||||
def download_file(file_name:, params: {}, request_headers: headers, path: maven_metadatum.path, group_id: group.id)
|
||||
get api("/groups/#{group_id}/-/packages/maven/#{path}/#{file_name}"), params: params, headers: request_headers
|
||||
end
|
||||
|
||||
def download_file_with_token(file_name:, params: {}, request_headers: headers_with_token, path: maven_metadatum.path, group_id: group.id)
|
||||
download_file(file_name: file_name, params: params, request_headers: request_headers, path: path, group_id: group_id)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'HEAD /api/v4/groups/:id/-/packages/maven/*path/:file_name' do
|
||||
let(:url) { "/groups/#{group.id}/-/packages/maven/#{package.maven_metadatum.path}/#{package_file.file_name}" }
|
||||
let(:path) { package.maven_metadatum.path }
|
||||
let(:url) { "/groups/#{group.id}/-/packages/maven/#{path}/#{package_file.file_name}" }
|
||||
|
||||
context 'with maven_packages_group_level_improvements enabled' do
|
||||
before do
|
||||
|
@ -538,12 +654,28 @@ RSpec.describe API::MavenPackages do
|
|||
|
||||
it_behaves_like 'processing HEAD requests'
|
||||
end
|
||||
|
||||
context 'with check_maven_path_first enabled' do
|
||||
before do
|
||||
stub_feature_flags(check_maven_path_first: true)
|
||||
end
|
||||
|
||||
it_behaves_like 'processing HEAD requests'
|
||||
end
|
||||
|
||||
context 'with check_maven_path_first disabled' do
|
||||
before do
|
||||
stub_feature_flags(check_maven_path_first: false)
|
||||
end
|
||||
|
||||
it_behaves_like 'processing HEAD requests'
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /api/v4/projects/:id/packages/maven/*path/:file_name' do
|
||||
shared_examples 'handling all conditions' do
|
||||
context 'a public project' do
|
||||
subject { download_file(package_file.file_name) }
|
||||
subject { download_file(file_name: package_file.file_name) }
|
||||
|
||||
it_behaves_like 'tracking the file download event'
|
||||
|
||||
|
@ -555,12 +687,18 @@ RSpec.describe API::MavenPackages do
|
|||
end
|
||||
|
||||
it 'returns sha1 of the file' do
|
||||
download_file(package_file.file_name + '.sha1')
|
||||
download_file(file_name: package_file.file_name + '.sha1')
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response.media_type).to eq('text/plain')
|
||||
expect(response.body).to eq(package_file.file_sha1)
|
||||
end
|
||||
|
||||
context 'with a non existing maven path' do
|
||||
subject { download_file(file_name: package_file.file_name, path: 'foo/bar/1.2.3') }
|
||||
|
||||
it_behaves_like 'rejecting the request for non existing maven path'
|
||||
end
|
||||
end
|
||||
|
||||
context 'private project' do
|
||||
|
@ -568,7 +706,7 @@ RSpec.describe API::MavenPackages do
|
|||
project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
|
||||
end
|
||||
|
||||
subject { download_file_with_token(package_file.file_name) }
|
||||
subject { download_file_with_token(file_name: package_file.file_name) }
|
||||
|
||||
it_behaves_like 'tracking the file download event'
|
||||
|
||||
|
@ -588,7 +726,7 @@ RSpec.describe API::MavenPackages do
|
|||
end
|
||||
|
||||
it 'denies download when no private token' do
|
||||
download_file(package_file.file_name)
|
||||
download_file(file_name: package_file.file_name)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
|
@ -596,6 +734,12 @@ RSpec.describe API::MavenPackages do
|
|||
it_behaves_like 'downloads with a job token'
|
||||
|
||||
it_behaves_like 'downloads with a deploy token'
|
||||
|
||||
context 'with a non existing maven path' do
|
||||
subject { download_file_with_token(file_name: package_file.file_name, path: 'foo/bar/1.2.3') }
|
||||
|
||||
it_behaves_like 'rejecting the request for non existing maven path'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -615,18 +759,35 @@ RSpec.describe API::MavenPackages do
|
|||
it_behaves_like 'handling all conditions'
|
||||
end
|
||||
|
||||
def download_file(file_name, params = {}, request_headers = headers)
|
||||
get api("/projects/#{project.id}/packages/maven/" \
|
||||
"#{maven_metadatum.path}/#{file_name}"), params: params, headers: request_headers
|
||||
context 'with check_maven_path_first enabled' do
|
||||
before do
|
||||
stub_feature_flags(check_maven_path_first: true)
|
||||
end
|
||||
|
||||
it_behaves_like 'handling all conditions'
|
||||
end
|
||||
|
||||
def download_file_with_token(file_name, params = {}, request_headers = headers_with_token)
|
||||
download_file(file_name, params, request_headers)
|
||||
context 'with check_maven_path_first disabled' do
|
||||
before do
|
||||
stub_feature_flags(check_maven_path_first: false)
|
||||
end
|
||||
|
||||
it_behaves_like 'handling all conditions'
|
||||
end
|
||||
|
||||
def download_file(file_name:, params: {}, request_headers: headers, path: maven_metadatum.path)
|
||||
get api("/projects/#{project.id}/packages/maven/" \
|
||||
"#{path}/#{file_name}"), params: params, headers: request_headers
|
||||
end
|
||||
|
||||
def download_file_with_token(file_name:, params: {}, request_headers: headers_with_token, path: maven_metadatum.path)
|
||||
download_file(file_name: file_name, params: params, request_headers: request_headers, path: path)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'HEAD /api/v4/projects/:id/packages/maven/*path/:file_name' do
|
||||
let(:url) { "/projects/#{project.id}/packages/maven/#{package.maven_metadatum.path}/#{package_file.file_name}" }
|
||||
let(:path) { package.maven_metadatum.path }
|
||||
let(:url) { "/projects/#{project.id}/packages/maven/#{path}/#{package_file.file_name}" }
|
||||
|
||||
context 'with maven_packages_group_level_improvements enabled' do
|
||||
before do
|
||||
|
@ -643,11 +804,27 @@ RSpec.describe API::MavenPackages do
|
|||
|
||||
it_behaves_like 'processing HEAD requests'
|
||||
end
|
||||
|
||||
context 'with check_maven_path_first enabled' do
|
||||
before do
|
||||
stub_feature_flags(check_maven_path_first: true)
|
||||
end
|
||||
|
||||
it_behaves_like 'processing HEAD requests'
|
||||
end
|
||||
|
||||
context 'with check_maven_path_first disabled' do
|
||||
before do
|
||||
stub_feature_flags(check_maven_path_first: false)
|
||||
end
|
||||
|
||||
it_behaves_like 'processing HEAD requests'
|
||||
end
|
||||
end
|
||||
|
||||
describe 'PUT /api/v4/projects/:id/packages/maven/*path/:file_name/authorize' do
|
||||
it 'rejects a malicious request' do
|
||||
put api("/projects/#{project.id}/packages/maven/com/example/my-app/#{version}/%2e%2e%2F.ssh%2Fauthorized_keys/authorize"), params: {}, headers: headers_with_token
|
||||
put api("/projects/#{project.id}/packages/maven/com/example/my-app/#{version}/%2e%2e%2F.ssh%2Fauthorized_keys/authorize"), headers: headers_with_token
|
||||
|
||||
expect(response).to have_gitlab_http_status(:bad_request)
|
||||
end
|
||||
|
|
|
@ -7,28 +7,77 @@ RSpec.describe MemberSerializer do
|
|||
|
||||
let_it_be(:current_user) { create(:user) }
|
||||
|
||||
subject { described_class.new.represent(members, { current_user: current_user, group: group, source: source }) }
|
||||
subject(:representation) do
|
||||
described_class.new.represent(members, { current_user: current_user, group: group, source: source }).to_json
|
||||
end
|
||||
|
||||
shared_examples 'members.json' do
|
||||
it 'matches json schema' do
|
||||
expect(subject.to_json).to match_schema('members')
|
||||
end
|
||||
it { is_expected.to match_schema('members') }
|
||||
end
|
||||
|
||||
context 'group member' do
|
||||
let(:group) { create(:group) }
|
||||
let_it_be(:group) { create(:group) }
|
||||
let_it_be(:members) { present_members(create_list(:group_member, 1, group: group)) }
|
||||
|
||||
let(:source) { group }
|
||||
let(:members) { present_members(create_list(:group_member, 1, group: group)) }
|
||||
|
||||
it_behaves_like 'members.json'
|
||||
|
||||
it 'handles last group owner assignment' do
|
||||
group_member = members.last
|
||||
|
||||
expect { representation }.to change(group_member, :last_owner)
|
||||
.from(nil).to(true)
|
||||
.and change(group_member, :last_blocked_owner).from(nil).to(false)
|
||||
end
|
||||
|
||||
context "with LastGroupOwnerAssigner query improvements" do
|
||||
it "avoids N+1 database queries for last group owner assignment in MembersPresenter" do
|
||||
group_member = create(:group_member, group: group)
|
||||
control_count = ActiveRecord::QueryRecorder.new { member_last_owner_with_preload([group_member]) }.count
|
||||
group_members = create_list(:group_member, 3, group: group)
|
||||
|
||||
expect { member_last_owner_with_preload(group_members) }.not_to exceed_query_limit(control_count)
|
||||
end
|
||||
|
||||
it "avoids N+1 database queries for last blocked owner assignment in MembersPresenter" do
|
||||
group_member = create(:group_member, group: group)
|
||||
control_count = ActiveRecord::QueryRecorder.new { member_last_blocked_owner_with_preload([group_member]) }.count
|
||||
group_members = create_list(:group_member, 3, group: group)
|
||||
|
||||
expect { member_last_blocked_owner_with_preload(group_members) }.not_to exceed_query_limit(control_count)
|
||||
end
|
||||
|
||||
def member_last_owner_with_preload(members)
|
||||
assigner_with_preload(members)
|
||||
members.map { |m| group.member_last_owner?(m) }
|
||||
end
|
||||
|
||||
def member_last_blocked_owner_with_preload(members)
|
||||
assigner_with_preload(members)
|
||||
members.map { |m| group.member_last_blocked_owner?(m) }
|
||||
end
|
||||
|
||||
def assigner_with_preload(members)
|
||||
MembersPreloader.new(members).preload_all
|
||||
Members::LastGroupOwnerAssigner.new(group, members).execute
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'project member' do
|
||||
let(:project) { create(:project) }
|
||||
let_it_be(:project) { create(:project) }
|
||||
let_it_be(:members) { present_members(create_list(:project_member, 1, project: project)) }
|
||||
|
||||
let(:source) { project }
|
||||
let(:group) { project.group }
|
||||
let(:members) { present_members(create_list(:project_member, 1, project: project)) }
|
||||
|
||||
it_behaves_like 'members.json'
|
||||
|
||||
it 'does not invoke group owner assignment' do
|
||||
expect(Members::LastGroupOwnerAssigner).not_to receive(:new)
|
||||
|
||||
representation
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -266,12 +266,6 @@ RSpec.configure do |config|
|
|||
|
||||
stub_feature_flags(unified_diff_components: false)
|
||||
|
||||
# Disable this feature flag as we iterate and
|
||||
# refactor filtered search to use gitlab ui
|
||||
# components to meet feature parody. More details found
|
||||
# https://gitlab.com/groups/gitlab-org/-/epics/5501
|
||||
stub_feature_flags(boards_filtered_search: false)
|
||||
|
||||
# The following `vue_issues_list` stub can be removed once the
|
||||
# Vue issues page has feature parity with the current Haml page
|
||||
stub_feature_flags(vue_issues_list: false)
|
||||
|
|
Loading…
Reference in New Issue