Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
1d42c38d9b
commit
15b3452054
|
@ -216,7 +216,6 @@ linters:
|
|||
- "app/views/projects/mattermosts/_team_selection.html.haml"
|
||||
- "app/views/projects/mattermosts/new.html.haml"
|
||||
- "app/views/projects/merge_requests/_commits.html.haml"
|
||||
- "app/views/projects/merge_requests/_discussion.html.haml"
|
||||
- "app/views/projects/merge_requests/_how_to_merge.html.haml"
|
||||
- "app/views/projects/merge_requests/_mr_title.html.haml"
|
||||
- "app/views/projects/merge_requests/conflicts/_commit_stats.html.haml"
|
||||
|
|
|
@ -1,65 +0,0 @@
|
|||
/* global CommentsStore */
|
||||
|
||||
import $ from 'jquery';
|
||||
import Vue from 'vue';
|
||||
import { __ } from '~/locale';
|
||||
|
||||
const CommentAndResolveBtn = Vue.extend({
|
||||
props: {
|
||||
discussionId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
textareaIsEmpty: true,
|
||||
discussion: {},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
showButton() {
|
||||
if (this.discussion) {
|
||||
return this.discussion.isResolvable();
|
||||
}
|
||||
return false;
|
||||
},
|
||||
isDiscussionResolved() {
|
||||
return this.discussion.isResolved();
|
||||
},
|
||||
buttonText() {
|
||||
if (this.textareaIsEmpty) {
|
||||
return this.isDiscussionResolved ? __('Unresolve thread') : __('Resolve thread');
|
||||
}
|
||||
return this.isDiscussionResolved
|
||||
? __('Comment & unresolve thread')
|
||||
: __('Comment & resolve thread');
|
||||
},
|
||||
},
|
||||
created() {
|
||||
if (this.discussionId) {
|
||||
this.discussion = CommentsStore.state[this.discussionId];
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (!this.discussionId) return;
|
||||
|
||||
const $textarea = $(
|
||||
`.js-discussion-note-form[data-discussion-id=${this.discussionId}] .note-textarea`,
|
||||
);
|
||||
this.textareaIsEmpty = $textarea.val() === '';
|
||||
|
||||
$textarea.on('input.comment-and-resolve-btn', () => {
|
||||
this.textareaIsEmpty = $textarea.val() === '';
|
||||
});
|
||||
},
|
||||
destroyed() {
|
||||
if (!this.discussionId) return;
|
||||
|
||||
$(`.js-discussion-note-form[data-discussion-id=${this.discussionId}] .note-textarea`).off(
|
||||
'input.comment-and-resolve-btn',
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
Vue.component('comment-and-resolve-btn', CommentAndResolveBtn);
|
|
@ -1,189 +0,0 @@
|
|||
/* global CommentsStore */
|
||||
|
||||
import $ from 'jquery';
|
||||
import Vue from 'vue';
|
||||
import collapseIcon from '../icons/collapse_icon.svg';
|
||||
import Notes from '../../notes';
|
||||
import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue';
|
||||
import { n__ } from '~/locale';
|
||||
|
||||
const DiffNoteAvatars = Vue.extend({
|
||||
components: {
|
||||
userAvatarImage,
|
||||
},
|
||||
props: {
|
||||
discussionId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isVisible: false,
|
||||
lineType: '',
|
||||
storeState: CommentsStore.state,
|
||||
shownAvatars: 3,
|
||||
collapseIcon,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
discussionClassName() {
|
||||
return `js-diff-avatars-${this.discussionId}`;
|
||||
},
|
||||
notesSubset() {
|
||||
let notes = [];
|
||||
|
||||
if (this.discussion) {
|
||||
notes = Object.keys(this.discussion.notes)
|
||||
.slice(0, this.shownAvatars)
|
||||
.map(noteId => this.discussion.notes[noteId]);
|
||||
}
|
||||
|
||||
return notes;
|
||||
},
|
||||
extraNotesTitle() {
|
||||
if (this.discussion) {
|
||||
const extra = this.discussion.notesCount() - this.shownAvatars;
|
||||
|
||||
return n__('%d more comment', '%d more comments', extra);
|
||||
}
|
||||
|
||||
return '';
|
||||
},
|
||||
discussion() {
|
||||
return this.storeState[this.discussionId];
|
||||
},
|
||||
notesCount() {
|
||||
if (this.discussion) {
|
||||
return this.discussion.notesCount();
|
||||
}
|
||||
|
||||
return 0;
|
||||
},
|
||||
moreText() {
|
||||
const plusSign = this.notesCount < 100 ? '+' : '';
|
||||
|
||||
return `${plusSign}${this.notesCount - this.shownAvatars}`;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
storeState: {
|
||||
handler() {
|
||||
this.$nextTick(() => {
|
||||
$('.has-tooltip', this.$el).tooltip('_fixTitle');
|
||||
|
||||
// We need to add/remove a class to an element that is outside the Vue instance
|
||||
this.addNoCommentClass();
|
||||
});
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$nextTick(() => {
|
||||
this.addNoCommentClass();
|
||||
this.setDiscussionVisible();
|
||||
|
||||
this.lineType = $(this.$el)
|
||||
.closest('.diff-line-num')
|
||||
.hasClass('old_line')
|
||||
? 'old'
|
||||
: 'new';
|
||||
});
|
||||
|
||||
$(document).on('toggle.comments', () => {
|
||||
this.$nextTick(() => {
|
||||
this.setDiscussionVisible();
|
||||
});
|
||||
});
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.addNoCommentClass();
|
||||
$(document).off('toggle.comments');
|
||||
},
|
||||
methods: {
|
||||
clickedAvatar(e) {
|
||||
Notes.instance.onAddDiffNote(e);
|
||||
|
||||
// Toggle the active state of the toggle all button
|
||||
this.toggleDiscussionsToggleState();
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.setDiscussionVisible();
|
||||
|
||||
$('.has-tooltip', this.$el).tooltip('_fixTitle');
|
||||
$('.has-tooltip', this.$el).tooltip('hide');
|
||||
});
|
||||
},
|
||||
addNoCommentClass() {
|
||||
const { notesCount } = this;
|
||||
|
||||
$(this.$el)
|
||||
.closest('.js-avatar-container')
|
||||
.toggleClass('no-comment-btn', notesCount > 0)
|
||||
.nextUntil('.js-avatar-container')
|
||||
.toggleClass('no-comment-btn', notesCount > 0);
|
||||
},
|
||||
toggleDiscussionsToggleState() {
|
||||
const $notesHolders = $(this.$el)
|
||||
.closest('.code')
|
||||
.find('.notes_holder');
|
||||
const $visibleNotesHolders = $notesHolders.filter(':visible');
|
||||
const $toggleDiffCommentsBtn = $(this.$el)
|
||||
.closest('.diff-file')
|
||||
.find('.js-toggle-diff-comments');
|
||||
|
||||
$toggleDiffCommentsBtn.toggleClass(
|
||||
'active',
|
||||
$notesHolders.length === $visibleNotesHolders.length,
|
||||
);
|
||||
},
|
||||
setDiscussionVisible() {
|
||||
this.isVisible = $(`.diffs .notes[data-discussion-id="${this.discussion.id}"]`).is(
|
||||
':visible',
|
||||
);
|
||||
},
|
||||
getTooltipText(note) {
|
||||
return `${note.authorName}: ${note.noteTruncated}`;
|
||||
},
|
||||
},
|
||||
template: `
|
||||
<div class="diff-comment-avatar-holders"
|
||||
:class="discussionClassName"
|
||||
v-show="notesCount !== 0">
|
||||
<div v-if="!isVisible">
|
||||
<!-- FIXME: Pass an alt attribute here for accessibility -->
|
||||
<user-avatar-image
|
||||
v-for="note in notesSubset"
|
||||
:key="note.id"
|
||||
class="diff-comment-avatar js-diff-comment-avatar"
|
||||
@click.native="clickedAvatar($event)"
|
||||
:img-src="note.authorAvatar"
|
||||
:tooltip-text="getTooltipText(note)"
|
||||
:data-line-type="lineType"
|
||||
:size="19"
|
||||
data-html="true"
|
||||
/>
|
||||
<span v-if="notesCount > shownAvatars"
|
||||
class="diff-comments-more-count has-tooltip js-diff-comment-avatar"
|
||||
data-container="body"
|
||||
data-placement="top"
|
||||
ref="extraComments"
|
||||
role="button"
|
||||
:data-line-type="lineType"
|
||||
:title="extraNotesTitle"
|
||||
@click="clickedAvatar($event)">{{ moreText }}</span>
|
||||
</div>
|
||||
<button class="diff-notes-collapse js-diff-comment-avatar"
|
||||
type="button"
|
||||
aria-label="Show comments"
|
||||
:data-line-type="lineType"
|
||||
@click="clickedAvatar($event)"
|
||||
v-if="isVisible"
|
||||
v-html="collapseIcon">
|
||||
</button>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
|
||||
Vue.component('diff-note-avatars', DiffNoteAvatars);
|
|
@ -1,210 +0,0 @@
|
|||
/* eslint-disable func-names, no-continue */
|
||||
/* global CommentsStore */
|
||||
|
||||
import $ from 'jquery';
|
||||
import 'vendor/jquery.scrollTo';
|
||||
import Vue from 'vue';
|
||||
import { __ } from '~/locale';
|
||||
|
||||
import DiscussionMixins from '../mixins/discussion';
|
||||
|
||||
const JumpToDiscussion = Vue.extend({
|
||||
mixins: [DiscussionMixins],
|
||||
props: {
|
||||
discussionId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
discussions: CommentsStore.state,
|
||||
discussion: {},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
buttonText() {
|
||||
if (this.discussionId) {
|
||||
return __('Jump to next unresolved thread');
|
||||
}
|
||||
return __('Jump to first unresolved thread');
|
||||
},
|
||||
allResolved() {
|
||||
return this.unresolvedDiscussionCount === 0;
|
||||
},
|
||||
showButton() {
|
||||
if (this.discussionId) {
|
||||
if (this.unresolvedDiscussionCount > 1) {
|
||||
return true;
|
||||
}
|
||||
return this.discussionId !== this.lastResolvedId;
|
||||
}
|
||||
return this.unresolvedDiscussionCount >= 1;
|
||||
},
|
||||
lastResolvedId() {
|
||||
let lastId;
|
||||
Object.keys(this.discussions).forEach(discussionId => {
|
||||
const discussion = this.discussions[discussionId];
|
||||
|
||||
if (!discussion.isResolved()) {
|
||||
lastId = discussion.id;
|
||||
}
|
||||
});
|
||||
return lastId;
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.discussion = this.discussions[this.discussionId];
|
||||
},
|
||||
methods: {
|
||||
jumpToNextUnresolvedDiscussion() {
|
||||
let discussionsSelector;
|
||||
let discussionIdsInScope;
|
||||
let firstUnresolvedDiscussionId;
|
||||
let nextUnresolvedDiscussionId;
|
||||
let activeTab = window.mrTabs.currentAction;
|
||||
let hasDiscussionsToJumpTo = true;
|
||||
let jumpToFirstDiscussion = !this.discussionId;
|
||||
|
||||
const discussionIdsForElements = function(elements) {
|
||||
return elements
|
||||
.map(function() {
|
||||
return $(this).attr('data-discussion-id');
|
||||
})
|
||||
.toArray();
|
||||
};
|
||||
|
||||
const { discussions } = this;
|
||||
|
||||
if (activeTab === 'diffs') {
|
||||
discussionsSelector = '.diffs .notes[data-discussion-id]';
|
||||
discussionIdsInScope = discussionIdsForElements($(discussionsSelector));
|
||||
|
||||
let unresolvedDiscussionCount = 0;
|
||||
|
||||
for (let i = 0; i < discussionIdsInScope.length; i += 1) {
|
||||
const discussionId = discussionIdsInScope[i];
|
||||
const discussion = discussions[discussionId];
|
||||
if (discussion && !discussion.isResolved()) {
|
||||
unresolvedDiscussionCount += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.discussionId && !this.discussion.isResolved()) {
|
||||
// If this is the last unresolved discussion on the diffs tab,
|
||||
// there are no discussions to jump to.
|
||||
if (unresolvedDiscussionCount === 1) {
|
||||
hasDiscussionsToJumpTo = false;
|
||||
}
|
||||
} else if (unresolvedDiscussionCount === 0) {
|
||||
// If there are no unresolved discussions on the diffs tab at all,
|
||||
// there are no discussions to jump to.
|
||||
hasDiscussionsToJumpTo = false;
|
||||
}
|
||||
} else if (activeTab !== 'show') {
|
||||
// If we are on the commits or builds tabs,
|
||||
// there are no discussions to jump to.
|
||||
hasDiscussionsToJumpTo = false;
|
||||
}
|
||||
|
||||
if (!hasDiscussionsToJumpTo) {
|
||||
// If there are no discussions to jump to on the current page,
|
||||
// switch to the notes tab and jump to the first discussion there.
|
||||
window.mrTabs.activateTab('show');
|
||||
activeTab = 'show';
|
||||
jumpToFirstDiscussion = true;
|
||||
}
|
||||
|
||||
if (activeTab === 'show') {
|
||||
discussionsSelector = '.discussion[data-discussion-id]';
|
||||
discussionIdsInScope = discussionIdsForElements($(discussionsSelector));
|
||||
}
|
||||
|
||||
let currentDiscussionFound = false;
|
||||
for (let i = 0; i < discussionIdsInScope.length; i += 1) {
|
||||
const discussionId = discussionIdsInScope[i];
|
||||
const discussion = discussions[discussionId];
|
||||
|
||||
if (!discussion) {
|
||||
// Discussions for comments on commits in this MR don't have a resolved status.
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!firstUnresolvedDiscussionId && !discussion.isResolved()) {
|
||||
firstUnresolvedDiscussionId = discussionId;
|
||||
|
||||
if (jumpToFirstDiscussion) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!jumpToFirstDiscussion) {
|
||||
if (currentDiscussionFound) {
|
||||
if (!discussion.isResolved()) {
|
||||
nextUnresolvedDiscussionId = discussionId;
|
||||
break;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (discussionId === this.discussionId) {
|
||||
currentDiscussionFound = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nextUnresolvedDiscussionId = nextUnresolvedDiscussionId || firstUnresolvedDiscussionId;
|
||||
|
||||
if (!nextUnresolvedDiscussionId) {
|
||||
return;
|
||||
}
|
||||
|
||||
let $target = $(`${discussionsSelector}[data-discussion-id="${nextUnresolvedDiscussionId}"]`);
|
||||
|
||||
if (activeTab === 'show') {
|
||||
$target = $target.closest('.note-discussion');
|
||||
|
||||
// If the next discussion is closed, toggle it open.
|
||||
if ($target.find('.js-toggle-content').is(':hidden')) {
|
||||
$target.find('.js-toggle-button i').trigger('click');
|
||||
}
|
||||
} else if (activeTab === 'diffs') {
|
||||
// Resolved discussions are hidden in the diffs tab by default.
|
||||
// If they are marked unresolved on the notes tab, they will still be hidden on the diffs tab.
|
||||
// When jumping between unresolved discussions on the diffs tab, we show them.
|
||||
$target.closest('.content').show();
|
||||
|
||||
const $notesHolder = $target.closest('tr.notes_holder');
|
||||
|
||||
// Image diff discussions does not use notes_holder
|
||||
// so we should keep original $target value in those cases
|
||||
if ($notesHolder.length > 0) {
|
||||
$target = $notesHolder;
|
||||
}
|
||||
|
||||
$target.show();
|
||||
|
||||
// If we are on the diffs tab, we don't scroll to the discussion itself, but to
|
||||
// 4 diff lines above it: the line the discussion was in response to + 3 context
|
||||
let prevEl;
|
||||
for (let i = 0; i < 4; i += 1) {
|
||||
prevEl = $target.prev();
|
||||
|
||||
// If the discussion doesn't have 4 lines above it, we'll have to do with fewer.
|
||||
if (!prevEl.hasClass('line_holder')) {
|
||||
break;
|
||||
}
|
||||
|
||||
$target = prevEl;
|
||||
}
|
||||
}
|
||||
|
||||
$.scrollTo($target, {
|
||||
offset: -150,
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Vue.component('jump-to-discussion', JumpToDiscussion);
|
|
@ -1,28 +0,0 @@
|
|||
/* global CommentsStore */
|
||||
|
||||
import Vue from 'vue';
|
||||
|
||||
const NewIssueForDiscussion = Vue.extend({
|
||||
props: {
|
||||
discussionId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
discussions: CommentsStore.state,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
discussion() {
|
||||
return this.discussions[this.discussionId];
|
||||
},
|
||||
showButton() {
|
||||
if (this.discussion) return !this.discussion.isResolved();
|
||||
return false;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Vue.component('new-issue-for-discussion-btn', NewIssueForDiscussion);
|
|
@ -1,145 +0,0 @@
|
|||
/* global CommentsStore */
|
||||
/* global ResolveService */
|
||||
|
||||
import $ from 'jquery';
|
||||
import Vue from 'vue';
|
||||
import { deprecatedCreateFlash as Flash } from '../../flash';
|
||||
import { sprintf, __ } from '~/locale';
|
||||
|
||||
const ResolveBtn = Vue.extend({
|
||||
props: {
|
||||
noteId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
discussionId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
resolved: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
canResolve: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
resolvedBy: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
authorName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
authorAvatar: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
noteTruncated: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
discussions: CommentsStore.state,
|
||||
loading: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
discussion() {
|
||||
return this.discussions[this.discussionId];
|
||||
},
|
||||
note() {
|
||||
return this.discussion ? this.discussion.getNote(this.noteId) : {};
|
||||
},
|
||||
buttonText() {
|
||||
if (this.isResolved) {
|
||||
return sprintf(__('Resolved by %{resolvedByName}'), {
|
||||
resolvedByName: this.resolvedByName,
|
||||
});
|
||||
} else if (this.canResolve) {
|
||||
return __('Mark as resolved');
|
||||
}
|
||||
|
||||
return __('Unable to resolve');
|
||||
},
|
||||
isResolved() {
|
||||
if (this.note) {
|
||||
return this.note.resolved;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
resolvedByName() {
|
||||
return this.note.resolved_by;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
discussions: {
|
||||
handler: 'updateTooltip',
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
$(this.$refs.button).tooltip({
|
||||
container: 'body',
|
||||
});
|
||||
},
|
||||
beforeDestroy() {
|
||||
CommentsStore.delete(this.discussionId, this.noteId);
|
||||
},
|
||||
created() {
|
||||
CommentsStore.create({
|
||||
discussionId: this.discussionId,
|
||||
noteId: this.noteId,
|
||||
canResolve: this.canResolve,
|
||||
resolved: this.resolved,
|
||||
resolvedBy: this.resolvedBy,
|
||||
authorName: this.authorName,
|
||||
authorAvatar: this.authorAvatar,
|
||||
noteTruncated: this.noteTruncated,
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
updateTooltip() {
|
||||
this.$nextTick(() => {
|
||||
$(this.$refs.button)
|
||||
.tooltip('hide')
|
||||
.tooltip('_fixTitle');
|
||||
});
|
||||
},
|
||||
resolve() {
|
||||
if (!this.canResolve) return;
|
||||
|
||||
let promise;
|
||||
this.loading = true;
|
||||
|
||||
if (this.isResolved) {
|
||||
promise = ResolveService.unresolve(this.noteId);
|
||||
} else {
|
||||
promise = ResolveService.resolve(this.noteId);
|
||||
}
|
||||
|
||||
promise
|
||||
.then(resp => resp.json())
|
||||
.then(data => {
|
||||
this.loading = false;
|
||||
|
||||
const resolvedBy = data ? data.resolved_by : null;
|
||||
|
||||
CommentsStore.update(this.discussionId, this.noteId, !this.isResolved, resolvedBy);
|
||||
this.discussion.updateHeadline(data);
|
||||
gl.mrWidget.checkStatus();
|
||||
this.updateTooltip();
|
||||
})
|
||||
.catch(
|
||||
() =>
|
||||
new Flash(__('An error occurred when trying to resolve a comment. Please try again.')),
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Vue.component('resolve-btn', ResolveBtn);
|
|
@ -1,28 +0,0 @@
|
|||
/* global CommentsStore */
|
||||
|
||||
import Vue from 'vue';
|
||||
|
||||
import DiscussionMixins from '../mixins/discussion';
|
||||
|
||||
window.ResolveCount = Vue.extend({
|
||||
mixins: [DiscussionMixins],
|
||||
props: {
|
||||
loggedOut: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
discussions: CommentsStore.state,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
allResolved() {
|
||||
return this.resolvedDiscussionCount === this.discussionCount;
|
||||
},
|
||||
resolvedCountText() {
|
||||
return this.discussionCount === 1 ? 'discussion' : 'discussions';
|
||||
},
|
||||
},
|
||||
});
|
|
@ -1,72 +0,0 @@
|
|||
/* eslint-disable func-names, new-cap */
|
||||
|
||||
import $ from 'jquery';
|
||||
import Vue from 'vue';
|
||||
import './models/discussion';
|
||||
import './models/note';
|
||||
import './stores/comments';
|
||||
import './services/resolve';
|
||||
import './mixins/discussion';
|
||||
import './components/comment_resolve_btn';
|
||||
import './components/jump_to_discussion';
|
||||
import './components/resolve_btn';
|
||||
import './components/resolve_count';
|
||||
import './components/diff_note_avatars';
|
||||
import './components/new_issue_for_discussion';
|
||||
|
||||
export default () => {
|
||||
const projectPathHolder =
|
||||
document.querySelector('.merge-request') || document.querySelector('.commit-box');
|
||||
const { projectPath } = projectPathHolder.dataset;
|
||||
const COMPONENT_SELECTOR =
|
||||
'resolve-btn, jump-to-discussion, comment-and-resolve-btn, new-issue-for-discussion-btn';
|
||||
|
||||
window.gl = window.gl || {};
|
||||
window.gl.diffNoteApps = {};
|
||||
|
||||
window.ResolveService = new gl.DiffNotesResolveServiceClass(projectPath);
|
||||
|
||||
gl.diffNotesCompileComponents = () => {
|
||||
$('diff-note-avatars').each(function() {
|
||||
const tmp = Vue.extend({
|
||||
template: $(this).get(0).outerHTML,
|
||||
});
|
||||
const tmpApp = new tmp().$mount();
|
||||
|
||||
$(this).replaceWith(tmpApp.$el);
|
||||
$(tmpApp.$el).one('remove.vue', () => {
|
||||
tmpApp.$destroy();
|
||||
tmpApp.$el.remove();
|
||||
});
|
||||
});
|
||||
|
||||
const $components = $(COMPONENT_SELECTOR).filter(function() {
|
||||
return $(this).closest('resolve-count').length !== 1;
|
||||
});
|
||||
|
||||
if ($components) {
|
||||
$components.each(function() {
|
||||
const $this = $(this);
|
||||
const noteId = $this.attr(':note-id');
|
||||
const discussionId = $this.attr(':discussion-id');
|
||||
|
||||
if ($this.is('comment-and-resolve-btn') && !discussionId) return;
|
||||
|
||||
const tmp = Vue.extend({
|
||||
template: $this.get(0).outerHTML,
|
||||
});
|
||||
const tmpApp = new tmp().$mount();
|
||||
|
||||
if (noteId) {
|
||||
gl.diffNoteApps[`note_${noteId}`] = tmpApp;
|
||||
}
|
||||
|
||||
$this.replaceWith(tmpApp.$el);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
gl.diffNotesCompileComponents();
|
||||
|
||||
$(window).trigger('resize.nav');
|
||||
};
|
|
@ -1 +0,0 @@
|
|||
<svg width="11" height="11" viewBox="0 0 9 13"><path d="M2.57568253,6.49866948 C2.50548852,6.57199715 2.44637866,6.59708255 2.39835118,6.57392645 C2.3503237,6.55077034 2.32631032,6.48902165 2.32631032,6.38867852 L2.32631032,-2.13272614 C2.32631032,-2.23306927 2.3503237,-2.29481796 2.39835118,-2.31797406 C2.44637866,-2.34113017 2.50548852,-2.31604477 2.57568253,-2.24271709 L6.51022184,1.86747129 C6.53977721,1.8983461 6.56379059,1.93500939 6.5822627,1.97746225 L6.5822627,2.27849013 C6.56379059,2.31708364 6.53977721,2.35374693 6.51022184,2.38848109 L2.57568253,6.49866948 Z" transform="translate(4.454287, 2.127976) rotate(90.000000) translate(-4.454287, -2.127976) "></path><path d="M3.74312342,2.09553332 C3.74312342,1.99519019 3.77821989,1.9083561 3.8484139,1.83502843 C3.91860791,1.76170075 4.00173115,1.72503747 4.09778611,1.72503747 L4.80711151,1.72503747 C4.90316647,1.72503747 4.98628971,1.76170075 5.05648372,1.83502843 C5.12667773,1.9083561 5.16177421,1.99519019 5.16177421,2.09553332 L5.16177421,10.2464421 C5.16177421,10.3467853 5.12667773,10.4336194 5.05648372,10.506947 C4.98628971,10.5802747 4.90316647,10.616938 4.80711151,10.616938 L4.09778611,10.616938 C4.00173115,10.616938 3.91860791,10.5802747 3.8484139,10.506947 C3.77821989,10.4336194 3.74312342,10.3467853 3.74312342,10.2464421 L3.74312342,2.09553332 Z" transform="translate(4.452449, 6.170988) rotate(-90.000000) translate(-4.452449, -6.170988) "></path><path d="M2.57568253,14.6236695 C2.50548852,14.6969971 2.44637866,14.7220826 2.39835118,14.6989264 C2.3503237,14.6757703 2.32631032,14.6140216 2.32631032,14.5136785 L2.32631032,5.99227386 C2.32631032,5.89193073 2.3503237,5.83018204 2.39835118,5.80702594 C2.44637866,5.78386983 2.50548852,5.80895523 2.57568253,5.88228291 L6.51022184,9.99247129 C6.53977721,10.0233461 6.56379059,10.0600094 6.5822627,10.1024622 L6.5822627,10.4034901 C6.56379059,10.4420836 6.53977721,10.4787469 6.51022184,10.5134811 L2.57568253,14.6236695 Z" transform="translate(4.454287, 10.252976) scale(1, -1) rotate(90.000000) translate(-4.454287, -10.252976) "></path></svg>
|
Before Width: | Height: | Size: 2.0 KiB |
|
@ -1,37 +0,0 @@
|
|||
/* eslint-disable guard-for-in, no-restricted-syntax, */
|
||||
|
||||
const DiscussionMixins = {
|
||||
computed: {
|
||||
discussionCount() {
|
||||
return Object.keys(this.discussions).length;
|
||||
},
|
||||
resolvedDiscussionCount() {
|
||||
let resolvedCount = 0;
|
||||
|
||||
for (const discussionId in this.discussions) {
|
||||
const discussion = this.discussions[discussionId];
|
||||
|
||||
if (discussion.isResolved()) {
|
||||
resolvedCount += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return resolvedCount;
|
||||
},
|
||||
unresolvedDiscussionCount() {
|
||||
let unresolvedCount = 0;
|
||||
|
||||
for (const discussionId in this.discussions) {
|
||||
const discussion = this.discussions[discussionId];
|
||||
|
||||
if (!discussion.isResolved()) {
|
||||
unresolvedCount += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return unresolvedCount;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default DiscussionMixins;
|
|
@ -1,99 +0,0 @@
|
|||
/* eslint-disable guard-for-in, no-restricted-syntax */
|
||||
/* global NoteModel */
|
||||
|
||||
import $ from 'jquery';
|
||||
import Vue from 'vue';
|
||||
import { localTimeAgo } from '../../lib/utils/datetime_utility';
|
||||
|
||||
class DiscussionModel {
|
||||
constructor(discussionId) {
|
||||
this.id = discussionId;
|
||||
this.notes = {};
|
||||
this.loading = false;
|
||||
this.canResolve = false;
|
||||
}
|
||||
|
||||
createNote(noteObj) {
|
||||
Vue.set(this.notes, noteObj.noteId, new NoteModel(this.id, noteObj));
|
||||
}
|
||||
|
||||
deleteNote(noteId) {
|
||||
Vue.delete(this.notes, noteId);
|
||||
}
|
||||
|
||||
getNote(noteId) {
|
||||
return this.notes[noteId];
|
||||
}
|
||||
|
||||
notesCount() {
|
||||
return Object.keys(this.notes).length;
|
||||
}
|
||||
|
||||
isResolved() {
|
||||
for (const noteId in this.notes) {
|
||||
const note = this.notes[noteId];
|
||||
|
||||
if (!note.resolved) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
resolveAllNotes(resolvedBy) {
|
||||
for (const noteId in this.notes) {
|
||||
const note = this.notes[noteId];
|
||||
|
||||
if (!note.resolved) {
|
||||
note.resolved = true;
|
||||
note.resolved_by = resolvedBy;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unResolveAllNotes() {
|
||||
for (const noteId in this.notes) {
|
||||
const note = this.notes[noteId];
|
||||
|
||||
if (note.resolved) {
|
||||
note.resolved = false;
|
||||
note.resolved_by = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateHeadline(data) {
|
||||
const discussionSelector = `.discussion[data-discussion-id="${this.id}"]`;
|
||||
const $discussionHeadline = $(`${discussionSelector} .js-discussion-headline`);
|
||||
|
||||
if (data.discussion_headline_html) {
|
||||
if ($discussionHeadline.length) {
|
||||
$discussionHeadline.replaceWith(data.discussion_headline_html);
|
||||
} else {
|
||||
$(`${discussionSelector} .discussion-header`).append(data.discussion_headline_html);
|
||||
}
|
||||
|
||||
localTimeAgo($('.js-timeago', `${discussionSelector}`));
|
||||
} else {
|
||||
$discussionHeadline.remove();
|
||||
}
|
||||
}
|
||||
|
||||
isResolvable() {
|
||||
if (!this.canResolve) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const noteId in this.notes) {
|
||||
const note = this.notes[noteId];
|
||||
|
||||
if (note.canResolve) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
window.DiscussionModel = DiscussionModel;
|
|
@ -1,14 +0,0 @@
|
|||
class NoteModel {
|
||||
constructor(discussionId, noteObj) {
|
||||
this.discussionId = discussionId;
|
||||
this.id = noteObj.noteId;
|
||||
this.canResolve = noteObj.canResolve;
|
||||
this.resolved = noteObj.resolved;
|
||||
this.resolved_by = noteObj.resolvedBy;
|
||||
this.authorName = noteObj.authorName;
|
||||
this.authorAvatar = noteObj.authorAvatar;
|
||||
this.noteTruncated = noteObj.noteTruncated;
|
||||
}
|
||||
}
|
||||
|
||||
window.NoteModel = NoteModel;
|
|
@ -1,86 +0,0 @@
|
|||
/* global CommentsStore */
|
||||
|
||||
import Vue from 'vue';
|
||||
import { deprecatedCreateFlash as Flash } from '../../flash';
|
||||
import { __ } from '~/locale';
|
||||
|
||||
window.gl = window.gl || {};
|
||||
|
||||
class ResolveServiceClass {
|
||||
constructor(root) {
|
||||
this.noteResource = Vue.resource(`${root}/notes{/noteId}/resolve?html=true`);
|
||||
this.discussionResource = Vue.resource(
|
||||
`${root}/merge_requests{/mergeRequestId}/discussions{/discussionId}/resolve?html=true`,
|
||||
);
|
||||
}
|
||||
|
||||
resolve(noteId) {
|
||||
return this.noteResource.save({ noteId }, {});
|
||||
}
|
||||
|
||||
unresolve(noteId) {
|
||||
return this.noteResource.delete({ noteId }, {});
|
||||
}
|
||||
|
||||
toggleResolveForDiscussion(mergeRequestId, discussionId) {
|
||||
const discussion = CommentsStore.state[discussionId];
|
||||
const isResolved = discussion.isResolved();
|
||||
let promise;
|
||||
|
||||
if (isResolved) {
|
||||
promise = this.unResolveAll(mergeRequestId, discussionId);
|
||||
} else {
|
||||
promise = this.resolveAll(mergeRequestId, discussionId);
|
||||
}
|
||||
|
||||
promise
|
||||
.then(resp => resp.json())
|
||||
.then(data => {
|
||||
discussion.loading = false;
|
||||
const resolvedBy = data ? data.resolved_by : null;
|
||||
|
||||
if (isResolved) {
|
||||
discussion.unResolveAllNotes();
|
||||
} else {
|
||||
discussion.resolveAllNotes(resolvedBy);
|
||||
}
|
||||
|
||||
if (gl.mrWidget) gl.mrWidget.checkStatus();
|
||||
discussion.updateHeadline(data);
|
||||
})
|
||||
.catch(
|
||||
() =>
|
||||
new Flash(__('An error occurred when trying to resolve a discussion. Please try again.')),
|
||||
);
|
||||
}
|
||||
|
||||
resolveAll(mergeRequestId, discussionId) {
|
||||
const discussion = CommentsStore.state[discussionId];
|
||||
|
||||
discussion.loading = true;
|
||||
|
||||
return this.discussionResource.save(
|
||||
{
|
||||
mergeRequestId,
|
||||
discussionId,
|
||||
},
|
||||
{},
|
||||
);
|
||||
}
|
||||
|
||||
unResolveAll(mergeRequestId, discussionId) {
|
||||
const discussion = CommentsStore.state[discussionId];
|
||||
|
||||
discussion.loading = true;
|
||||
|
||||
return this.discussionResource.delete(
|
||||
{
|
||||
mergeRequestId,
|
||||
discussionId,
|
||||
},
|
||||
{},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
gl.DiffNotesResolveServiceClass = ResolveServiceClass;
|
|
@ -1,56 +0,0 @@
|
|||
/* eslint-disable no-restricted-syntax, guard-for-in */
|
||||
/* global DiscussionModel */
|
||||
|
||||
import Vue from 'vue';
|
||||
|
||||
window.CommentsStore = {
|
||||
state: {},
|
||||
get(discussionId, noteId) {
|
||||
return this.state[discussionId].getNote(noteId);
|
||||
},
|
||||
createDiscussion(discussionId, canResolve) {
|
||||
let discussion = this.state[discussionId];
|
||||
if (!this.state[discussionId]) {
|
||||
discussion = new DiscussionModel(discussionId);
|
||||
Vue.set(this.state, discussionId, discussion);
|
||||
}
|
||||
|
||||
if (canResolve !== undefined) {
|
||||
discussion.canResolve = canResolve;
|
||||
}
|
||||
|
||||
return discussion;
|
||||
},
|
||||
create(noteObj) {
|
||||
const discussion = this.createDiscussion(noteObj.discussionId);
|
||||
|
||||
discussion.createNote(noteObj);
|
||||
},
|
||||
update(discussionId, noteId, resolved, resolvedBy) {
|
||||
const discussion = this.state[discussionId];
|
||||
const note = discussion.getNote(noteId);
|
||||
note.resolved = resolved;
|
||||
note.resolved_by = resolvedBy;
|
||||
},
|
||||
delete(discussionId, noteId) {
|
||||
const discussion = this.state[discussionId];
|
||||
discussion.deleteNote(noteId);
|
||||
|
||||
if (discussion.notesCount() === 0) {
|
||||
Vue.delete(this.state, discussionId);
|
||||
}
|
||||
},
|
||||
unresolvedDiscussionIds() {
|
||||
const ids = [];
|
||||
|
||||
for (const discussionId in this.state) {
|
||||
const discussion = this.state[discussionId];
|
||||
|
||||
if (!discussion.isResolved()) {
|
||||
ids.push(discussion.id);
|
||||
}
|
||||
}
|
||||
|
||||
return ids;
|
||||
},
|
||||
};
|
|
@ -1,32 +1,26 @@
|
|||
<script>
|
||||
import { mapActions, mapGetters, mapState } from 'vuex';
|
||||
import { escape } from 'lodash';
|
||||
import { GlButton, GlLoadingIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
|
||||
import { GlLoadingIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
|
||||
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import { sprintf } from '~/locale';
|
||||
import { __, sprintf } from '~/locale';
|
||||
import { deprecatedCreateFlash as createFlash } from '~/flash';
|
||||
import { hasDiff } from '~/helpers/diffs_helper';
|
||||
import eventHub from '../../notes/event_hub';
|
||||
import DiffFileHeader from './diff_file_header.vue';
|
||||
import DiffContent from './diff_content.vue';
|
||||
import { diffViewerErrors } from '~/ide/constants';
|
||||
import { GENERIC_ERROR, DIFF_FILE } from '../i18n';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
DiffFileHeader,
|
||||
DiffContent,
|
||||
GlButton,
|
||||
GlLoadingIcon,
|
||||
},
|
||||
directives: {
|
||||
SafeHtml,
|
||||
},
|
||||
mixins: [glFeatureFlagsMixin()],
|
||||
i18n: {
|
||||
genericError: GENERIC_ERROR,
|
||||
...DIFF_FILE,
|
||||
},
|
||||
props: {
|
||||
file: {
|
||||
type: Object,
|
||||
|
@ -59,7 +53,7 @@ export default {
|
|||
...mapGetters('diffs', ['getDiffFileDiscussions']),
|
||||
viewBlobLink() {
|
||||
return sprintf(
|
||||
this.$options.i18n.blobView,
|
||||
__('You can %{linkStart}view the blob%{linkEnd} instead.'),
|
||||
{
|
||||
linkStart: `<a href="${escape(this.file.view_path)}">`,
|
||||
linkEnd: '</a>',
|
||||
|
@ -81,7 +75,9 @@ export default {
|
|||
},
|
||||
forkMessage() {
|
||||
return sprintf(
|
||||
this.$options.i18n.editInFork,
|
||||
__(
|
||||
"You're not allowed to %{tag_start}edit%{tag_end} files in this project directly. Please fork this project, make your changes there, and submit a merge request.",
|
||||
),
|
||||
{
|
||||
tag_start: '<span class="js-file-fork-suggestion-section-action">',
|
||||
tag_end: '</span>',
|
||||
|
@ -152,7 +148,7 @@ export default {
|
|||
})
|
||||
.catch(() => {
|
||||
this.isLoadingCollapsedDiff = false;
|
||||
createFlash(this.$options.i18n.genericError);
|
||||
createFlash(__('Something went wrong on our end. Please try again!'));
|
||||
});
|
||||
},
|
||||
showForkMessage() {
|
||||
|
@ -192,14 +188,14 @@ export default {
|
|||
<a
|
||||
:href="file.fork_path"
|
||||
class="js-fork-suggestion-button btn btn-grouped btn-inverted btn-success"
|
||||
>{{ $options.i18n.fork }}</a
|
||||
>{{ __('Fork') }}</a
|
||||
>
|
||||
<button
|
||||
class="js-cancel-fork-suggestion-button btn btn-grouped"
|
||||
type="button"
|
||||
@click="hideForkMessage"
|
||||
>
|
||||
{{ $options.i18n.cancel }}
|
||||
{{ __('Cancel') }}
|
||||
</button>
|
||||
</div>
|
||||
<gl-loading-icon v-if="showLoadingIcon" class="diff-content loading" />
|
||||
|
@ -209,16 +205,11 @@ export default {
|
|||
<div v-safe-html="errorMessage" class="nothing-here-block"></div>
|
||||
</div>
|
||||
<template v-else>
|
||||
<div
|
||||
v-show="isCollapsed"
|
||||
class="gl-p-7 gl-bg-gray-10 gl-text-center collapsed-file-warning"
|
||||
>
|
||||
<p class="gl-mb-8 gl-mt-5">
|
||||
{{ $options.i18n.collapsed }}
|
||||
</p>
|
||||
<gl-button class="gl-mb-5" data-testid="expandButton" @click="handleToggle">
|
||||
{{ $options.i18n.expand }}
|
||||
</gl-button>
|
||||
<div v-show="isCollapsed" class="nothing-here-block diff-collapsed">
|
||||
{{ __('This diff is collapsed.') }}
|
||||
<a class="click-to-expand js-click-to-expand" href="#" @click.prevent="handleToggle">{{
|
||||
__('Click to expand it.')
|
||||
}}</a>
|
||||
</div>
|
||||
<diff-content
|
||||
v-show="!isCollapsed && !isFileTooLarge"
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
import { __ } from '~/locale';
|
||||
|
||||
export const GENERIC_ERROR = __('Something went wrong on our end. Please try again!');
|
||||
|
||||
export const DIFF_FILE = {
|
||||
blobView: __('You can %{linkStart}view the blob%{linkEnd} instead.'),
|
||||
editInFork: __(
|
||||
"You're not allowed to %{tag_start}edit%{tag_end} files in this project directly. Please fork this project, make your changes there, and submit a merge request.",
|
||||
),
|
||||
fork: __('Fork'),
|
||||
cancel: __('Cancel'),
|
||||
collapsed: __('This file is collapsed.'),
|
||||
expand: __('Expand file'),
|
||||
};
|
|
@ -0,0 +1,60 @@
|
|||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { GlModal } from '@gitlab/ui';
|
||||
import { __ } from '~/locale';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlModal,
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['isSavingOrTesting']),
|
||||
primaryProps() {
|
||||
return {
|
||||
text: __('Save'),
|
||||
attributes: [
|
||||
{ variant: 'success' },
|
||||
{ category: 'primary' },
|
||||
{ disabled: this.isSavingOrTesting },
|
||||
],
|
||||
};
|
||||
},
|
||||
cancelProps() {
|
||||
return {
|
||||
text: __('Cancel'),
|
||||
};
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onSubmit() {
|
||||
this.$emit('submit');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-modal
|
||||
modal-id="confirmSaveIntegration"
|
||||
size="sm"
|
||||
:title="s__('Integrations|Save settings?')"
|
||||
:action-primary="primaryProps"
|
||||
:action-cancel="cancelProps"
|
||||
@primary="onSubmit"
|
||||
>
|
||||
<p>
|
||||
{{
|
||||
s__(
|
||||
'Integrations|Saving will update the default settings for all projects that are not using custom settings.',
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
<p class="gl-mb-0">
|
||||
{{
|
||||
s__(
|
||||
'Integrations|Projects using custom settings will not be impacted unless the project owner chooses to use instance-level defaults.',
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
</gl-modal>
|
||||
</template>
|
|
@ -1,8 +1,9 @@
|
|||
<script>
|
||||
import { mapState, mapActions, mapGetters } from 'vuex';
|
||||
import { GlButton } from '@gitlab/ui';
|
||||
import { GlButton, GlModalDirective } from '@gitlab/ui';
|
||||
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import eventHub from '../event_hub';
|
||||
import { integrationLevels } from '../constants';
|
||||
|
||||
import OverrideDropdown from './override_dropdown.vue';
|
||||
import ActiveCheckbox from './active_checkbox.vue';
|
||||
|
@ -10,6 +11,7 @@ import JiraTriggerFields from './jira_trigger_fields.vue';
|
|||
import JiraIssuesFields from './jira_issues_fields.vue';
|
||||
import TriggerFields from './trigger_fields.vue';
|
||||
import DynamicField from './dynamic_field.vue';
|
||||
import ConfirmationModal from './confirmation_modal.vue';
|
||||
|
||||
export default {
|
||||
name: 'IntegrationForm',
|
||||
|
@ -20,8 +22,12 @@ export default {
|
|||
JiraIssuesFields,
|
||||
TriggerFields,
|
||||
DynamicField,
|
||||
ConfirmationModal,
|
||||
GlButton,
|
||||
},
|
||||
directives: {
|
||||
'gl-modal': GlModalDirective,
|
||||
},
|
||||
mixins: [glFeatureFlagsMixin()],
|
||||
computed: {
|
||||
...mapGetters(['currentKey', 'propsSource', 'isSavingOrTesting']),
|
||||
|
@ -32,6 +38,9 @@ export default {
|
|||
isJira() {
|
||||
return this.propsSource.type === 'jira';
|
||||
},
|
||||
isInstanceLevel() {
|
||||
return this.propsSource.integrationLevel === integrationLevels.INSTANCE;
|
||||
},
|
||||
showJiraIssuesFields() {
|
||||
return this.isJira && this.glFeatures.jiraIssuesIntegration;
|
||||
},
|
||||
|
@ -82,7 +91,21 @@ export default {
|
|||
v-bind="propsSource.jiraIssuesProps"
|
||||
/>
|
||||
<div v-if="isEditable" class="footer-block row-content-block">
|
||||
<template v-if="isInstanceLevel">
|
||||
<gl-button
|
||||
v-gl-modal.confirmSaveIntegration
|
||||
category="primary"
|
||||
variant="success"
|
||||
:loading="isSaving"
|
||||
:disabled="isSavingOrTesting"
|
||||
data-qa-selector="save_changes_button"
|
||||
>
|
||||
{{ __('Save changes') }}
|
||||
</gl-button>
|
||||
<confirmation-modal @submit="onSaveClick" />
|
||||
</template>
|
||||
<gl-button
|
||||
v-else
|
||||
category="primary"
|
||||
variant="success"
|
||||
type="submit"
|
||||
|
@ -93,6 +116,7 @@ export default {
|
|||
>
|
||||
{{ __('Save changes') }}
|
||||
</gl-button>
|
||||
|
||||
<gl-button
|
||||
v-if="propsSource.canTest"
|
||||
:loading="isTesting"
|
||||
|
|
|
@ -396,10 +396,6 @@ export default class MergeRequestTabs {
|
|||
|
||||
initChangesDropdown(this.stickyTop);
|
||||
|
||||
if (typeof gl.diffNotesCompileComponents !== 'undefined') {
|
||||
gl.diffNotesCompileComponents();
|
||||
}
|
||||
|
||||
localTimeAgo($('.js-timeago', 'div#diffs'));
|
||||
syntaxHighlight($('#diffs .js-syntax-highlight'));
|
||||
|
||||
|
|
|
@ -479,11 +479,6 @@ export default class Notes {
|
|||
row = form;
|
||||
}
|
||||
|
||||
const lineType = this.isParallelView() ? form.find('#line_type').val() : 'old';
|
||||
const diffAvatarContainer = row
|
||||
.prevAll('.line_holder')
|
||||
.first()
|
||||
.find(`.js-avatar-container.${lineType}_line`);
|
||||
// is this the first note of discussion?
|
||||
discussionContainer = $(`.notes[data-discussion-id="${noteEntity.discussion_id}"]`);
|
||||
if (!discussionContainer.length) {
|
||||
|
@ -519,12 +514,6 @@ export default class Notes {
|
|||
Notes.animateAppendNote(noteEntity.html, discussionContainer);
|
||||
}
|
||||
|
||||
if (typeof gl.diffNotesCompileComponents !== 'undefined' && noteEntity.discussion_resolvable) {
|
||||
gl.diffNotesCompileComponents();
|
||||
|
||||
this.renderDiscussionAvatar(diffAvatarContainer, noteEntity);
|
||||
}
|
||||
|
||||
localTimeAgo($('.js-timeago'), false);
|
||||
Notes.checkMergeRequestStatus();
|
||||
return this.updateNotesCount(1);
|
||||
|
@ -538,19 +527,6 @@ export default class Notes {
|
|||
.get(0);
|
||||
}
|
||||
|
||||
renderDiscussionAvatar(diffAvatarContainer, noteEntity) {
|
||||
let avatarHolder = diffAvatarContainer.find('.diff-comment-avatar-holders');
|
||||
|
||||
if (!avatarHolder.length) {
|
||||
avatarHolder = document.createElement('diff-note-avatars');
|
||||
avatarHolder.setAttribute('discussion-id', noteEntity.discussion_id);
|
||||
|
||||
diffAvatarContainer.append(avatarHolder);
|
||||
|
||||
gl.diffNotesCompileComponents();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called in response the main target form has been successfully submitted.
|
||||
*
|
||||
|
@ -605,10 +581,6 @@ export default class Notes {
|
|||
form.find('#note_type').val('');
|
||||
form.find('#note_project_id').remove();
|
||||
form.find('#in_reply_to_discussion_id').remove();
|
||||
form
|
||||
.find('.js-comment-resolve-button')
|
||||
.closest('comment-and-resolve-btn')
|
||||
.remove();
|
||||
this.parentTimeline = form.parents('.timeline');
|
||||
|
||||
if (form.length) {
|
||||
|
@ -714,10 +686,6 @@ export default class Notes {
|
|||
|
||||
$note_li.replaceWith($noteEntityEl);
|
||||
this.setupNewNote($noteEntityEl);
|
||||
|
||||
if (typeof gl.diffNotesCompileComponents !== 'undefined') {
|
||||
gl.diffNotesCompileComponents();
|
||||
}
|
||||
}
|
||||
|
||||
checkContentToAllowEditing($el) {
|
||||
|
@ -844,12 +812,6 @@ export default class Notes {
|
|||
const $notes = $note.closest('.discussion-notes');
|
||||
const discussionId = $('.notes', $notes).data('discussionId');
|
||||
|
||||
if (typeof gl.diffNotesCompileComponents !== 'undefined') {
|
||||
if (gl.diffNoteApps[noteElId]) {
|
||||
gl.diffNoteApps[noteElId].$destroy();
|
||||
}
|
||||
}
|
||||
|
||||
$note.remove();
|
||||
|
||||
// check if this is the last note for this line
|
||||
|
@ -979,13 +941,6 @@ export default class Notes {
|
|||
|
||||
form.removeClass('js-main-target-form').addClass('discussion-form js-discussion-note-form');
|
||||
|
||||
if (typeof gl.diffNotesCompileComponents !== 'undefined') {
|
||||
const $commentBtn = form.find('comment-and-resolve-btn');
|
||||
$commentBtn.attr(':discussion-id', `'${discussionID}'`);
|
||||
|
||||
gl.diffNotesCompileComponents();
|
||||
}
|
||||
|
||||
form.find('.js-note-text').focus();
|
||||
form.find('.js-comment-resolve-button').attr('data-discussion-id', discussionID);
|
||||
}
|
||||
|
|
|
@ -1,11 +1,8 @@
|
|||
<script>
|
||||
/* eslint-disable vue/no-v-html */
|
||||
import Vue from 'vue';
|
||||
import Cookies from 'js-cookie';
|
||||
import { GlIcon } from '@gitlab/ui';
|
||||
import Translate from '../../../../../vue_shared/translate';
|
||||
// Full path is needed for Jest to be able to correctly mock this file
|
||||
import illustrationSvg from '~/pages/projects/pipeline_schedules/shared/icons/intro_illustration.svg';
|
||||
import { parseBoolean } from '~/lib/utils/common_utils';
|
||||
|
||||
Vue.use(Translate);
|
||||
|
@ -20,12 +17,10 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
docsUrl: document.getElementById('pipeline-schedules-callout').dataset.docsUrl,
|
||||
imageUrl: document.getElementById('pipeline-schedules-callout').dataset.imageUrl,
|
||||
calloutDismissed: parseBoolean(Cookies.get(cookieKey)),
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.illustrationSvg = illustrationSvg;
|
||||
},
|
||||
methods: {
|
||||
dismissCallout() {
|
||||
this.calloutDismissed = true;
|
||||
|
@ -40,7 +35,9 @@ export default {
|
|||
<button id="dismiss-callout-btn" class="btn btn-default close" @click="dismissCallout">
|
||||
<gl-icon name="close" aria-hidden="true" />
|
||||
</button>
|
||||
<div class="svg-container" v-html="illustrationSvg"></div>
|
||||
<div class="svg-container">
|
||||
<img :src="imageUrl" />
|
||||
</div>
|
||||
<div class="user-callout-copy">
|
||||
<h4>{{ __('Scheduling Pipelines') }}</h4>
|
||||
<p>
|
||||
|
|
|
@ -57,16 +57,10 @@ export default class SingleFileDiff {
|
|||
this.content.hide();
|
||||
this.$toggleIcon.addClass('fa-caret-right').removeClass('fa-caret-down');
|
||||
this.collapsedContent.show();
|
||||
if (typeof gl.diffNotesCompileComponents !== 'undefined') {
|
||||
gl.diffNotesCompileComponents();
|
||||
}
|
||||
} else if (this.content) {
|
||||
this.collapsedContent.hide();
|
||||
this.content.show();
|
||||
this.$toggleIcon.addClass('fa-caret-down').removeClass('fa-caret-right');
|
||||
if (typeof gl.diffNotesCompileComponents !== 'undefined') {
|
||||
gl.diffNotesCompileComponents();
|
||||
}
|
||||
} else {
|
||||
this.$toggleIcon.addClass('fa-caret-down').removeClass('fa-caret-right');
|
||||
return this.getContentHTML(cb);
|
||||
|
@ -90,10 +84,6 @@ export default class SingleFileDiff {
|
|||
}
|
||||
this.collapsedContent.after(this.content);
|
||||
|
||||
if (typeof gl.diffNotesCompileComponents !== 'undefined') {
|
||||
gl.diffNotesCompileComponents();
|
||||
}
|
||||
|
||||
const $file = $(this.file);
|
||||
FilesCommentButton.init($file);
|
||||
|
||||
|
|
|
@ -9,6 +9,12 @@ const fileExtensionIcons = {
|
|||
'md.rendered': 'markdown',
|
||||
markdown: 'markdown',
|
||||
'markdown.rendered': 'markdown',
|
||||
mdown: 'markdown',
|
||||
'mdown.rendered': 'markdown',
|
||||
mkd: 'markdown',
|
||||
'mkd.rendered': 'markdown',
|
||||
mkdn: 'markdown',
|
||||
'mkdn.rendered': 'markdown',
|
||||
rst: 'markdown',
|
||||
blink: 'blink',
|
||||
css: 'css',
|
||||
|
|
|
@ -123,20 +123,13 @@
|
|||
}
|
||||
|
||||
.build-header {
|
||||
.ci-header-container,
|
||||
.header-action-buttons {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.ci-header-container {
|
||||
min-height: 54px;
|
||||
}
|
||||
|
||||
.page-content-header {
|
||||
padding: 10px 0 9px;
|
||||
}
|
||||
|
||||
.header-action-buttons {
|
||||
display: flex;
|
||||
|
||||
@include media-breakpoint-down(xs) {
|
||||
.sidebar-toggle-btn {
|
||||
margin-top: 0;
|
||||
|
|
|
@ -1013,31 +1013,35 @@ button.mini-pipeline-graph-dropdown-toggle {
|
|||
/**
|
||||
* Terminal
|
||||
*/
|
||||
.terminal-icon {
|
||||
margin-left: 3px;
|
||||
}
|
||||
[data-page='projects:jobs:terminal'],
|
||||
[data-page='projects:environments:terminal'] {
|
||||
.terminal-container {
|
||||
.content-block {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.terminal-container {
|
||||
.content-block {
|
||||
border-bottom: 0;
|
||||
}
|
||||
#terminal {
|
||||
margin-top: 10px;
|
||||
|
||||
#terminal {
|
||||
margin-top: 10px;
|
||||
min-height: 450px;
|
||||
box-sizing: border-box;
|
||||
|
||||
> div {
|
||||
min-height: 450px;
|
||||
> div {
|
||||
min-height: 450px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ci-header-container {
|
||||
min-height: 55px;
|
||||
/**
|
||||
* Pipelines / Jobs header
|
||||
*/
|
||||
[data-page='projects:pipelines:show'],
|
||||
[data-page='projects:jobs:show'] {
|
||||
.ci-header-container {
|
||||
min-height: $gl-spacing-scale-7;
|
||||
display: flex;
|
||||
|
||||
.text-center {
|
||||
padding-top: 12px;
|
||||
.text-center {
|
||||
padding-top: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -13,13 +13,14 @@ class InvitesController < ApplicationController
|
|||
respond_to :html
|
||||
|
||||
def show
|
||||
track_experiment('opened')
|
||||
track_new_user_invite_experiment('opened')
|
||||
accept if skip_invitation_prompt?
|
||||
end
|
||||
|
||||
def accept
|
||||
if member.accept_invite!(current_user)
|
||||
track_experiment('accepted')
|
||||
track_new_user_invite_experiment('accepted')
|
||||
track_invitation_reminders_experiment('accepted')
|
||||
redirect_to invite_details[:path], notice: _("You have been granted %{member_human_access} access to %{title} %{name}.") %
|
||||
{ member_human_access: member.human_access, title: invite_details[:title], name: invite_details[:name] }
|
||||
else
|
||||
|
@ -105,13 +106,25 @@ class InvitesController < ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
def track_experiment(action)
|
||||
def track_new_user_invite_experiment(action)
|
||||
return unless params[:new_user_invite]
|
||||
|
||||
property = params[:new_user_invite] == 'experiment' ? 'experiment_group' : 'control_group'
|
||||
|
||||
track_experiment(:invite_email, action, property)
|
||||
end
|
||||
|
||||
def track_invitation_reminders_experiment(action)
|
||||
return unless Gitlab::Experimentation.enabled?(:invitation_reminders)
|
||||
|
||||
property = Gitlab::Experimentation.enabled_for_attribute?(:invitation_reminders, member.invite_email) ? 'experimental_group' : 'control_group'
|
||||
|
||||
track_experiment(:invitation_reminders, action, property)
|
||||
end
|
||||
|
||||
def track_experiment(experiment_key, action, property)
|
||||
Gitlab::Tracking.event(
|
||||
Gitlab::Experimentation::EXPERIMENTS[:invite_email][:tracking_category],
|
||||
Gitlab::Experimentation.experiment(experiment_key).tracking_category,
|
||||
action,
|
||||
property: property,
|
||||
label: Digest::MD5.hexdigest(member.to_global_id.to_s)
|
||||
|
|
|
@ -77,6 +77,15 @@ module Emails
|
|||
Gitlab::Tracking.event(Gitlab::Experimentation::EXPERIMENTS[:invite_email][:tracking_category], 'sent', property: 'control_group')
|
||||
end
|
||||
end
|
||||
|
||||
if member.invite_to_unknown_user? && Gitlab::Experimentation.enabled?(:invitation_reminders)
|
||||
Gitlab::Tracking.event(
|
||||
Gitlab::Experimentation.experiment(:invitation_reminders).tracking_category,
|
||||
'sent',
|
||||
property: Gitlab::Experimentation.enabled_for_attribute?(:invitation_reminders, member.invite_email) ? 'experimental_group' : 'control_group',
|
||||
label: Digest::MD5.hexdigest(member.to_global_id.to_s)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def member_invite_accepted_email(member_source_type, member_id)
|
||||
|
|
|
@ -42,6 +42,7 @@ module Ci
|
|||
has_many :stages, -> { order(position: :asc) }, inverse_of: :pipeline
|
||||
has_many :statuses, class_name: 'CommitStatus', foreign_key: :commit_id, inverse_of: :pipeline
|
||||
has_many :latest_statuses_ordered_by_stage, -> { latest.order(:stage_idx, :stage) }, class_name: 'CommitStatus', foreign_key: :commit_id, inverse_of: :pipeline
|
||||
has_many :latest_statuses, -> { latest }, class_name: 'CommitStatus', foreign_key: :commit_id, inverse_of: :pipeline
|
||||
has_many :processables, class_name: 'Ci::Processable', foreign_key: :commit_id, inverse_of: :pipeline
|
||||
has_many :bridges, class_name: 'Ci::Bridge', foreign_key: :commit_id, inverse_of: :pipeline
|
||||
has_many :builds, foreign_key: :commit_id, inverse_of: :pipeline
|
||||
|
@ -577,11 +578,11 @@ module Ci
|
|||
end
|
||||
|
||||
def retried
|
||||
@retried ||= (statuses.order(id: :desc) - statuses.latest)
|
||||
@retried ||= (statuses.order(id: :desc) - latest_statuses)
|
||||
end
|
||||
|
||||
def coverage
|
||||
coverage_array = statuses.latest.map(&:coverage).compact
|
||||
coverage_array = latest_statuses.map(&:coverage).compact
|
||||
if coverage_array.size >= 1
|
||||
'%.2f' % (coverage_array.reduce(:+) / coverage_array.size)
|
||||
end
|
||||
|
|
|
@ -102,7 +102,7 @@ module Clusters
|
|||
def terminals(environment, data)
|
||||
pods = filter_by_project_environment(data[:pods], environment.project.full_path_slug, environment.slug)
|
||||
terminals = pods.flat_map { |pod| terminals_for_pod(api_url, environment.deployment_namespace, pod) }.compact
|
||||
terminals.each { |terminal| add_terminal_auth(terminal, terminal_auth) }
|
||||
terminals.each { |terminal| add_terminal_auth(terminal, **terminal_auth) }
|
||||
end
|
||||
|
||||
def kubeclient
|
||||
|
|
|
@ -78,7 +78,10 @@ class Member < ApplicationRecord
|
|||
scope :request, -> { where.not(requested_at: nil) }
|
||||
scope :non_request, -> { where(requested_at: nil) }
|
||||
|
||||
scope :not_accepted_invitations_by_user, -> (user) { invite.where(invite_accepted_at: nil, created_by: user) }
|
||||
scope :not_accepted_invitations, -> { invite.where(invite_accepted_at: nil) }
|
||||
scope :not_accepted_invitations_by_user, -> (user) { not_accepted_invitations.where(created_by: user) }
|
||||
scope :not_expired, -> (today = Date.current) { where(arel_table[:expires_at].gt(today).or(arel_table[:expires_at].eq(nil))) }
|
||||
scope :last_ten_days_excluding_today, -> (today = Date.current) { where(created_at: (today - 10).beginning_of_day..(today - 1).end_of_day) }
|
||||
|
||||
scope :has_access, -> { active.where('access_level > 0') }
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ class MergeRequestPollCachedWidgetEntity < IssuableEntity
|
|||
expose :target_project_id
|
||||
expose :squash
|
||||
expose :rebase_in_progress?, as: :rebase_in_progress
|
||||
expose :default_squash_commit_message, if: -> (merge_request, _) { merge_request.mergeable? }
|
||||
expose :default_squash_commit_message
|
||||
expose :commits_count
|
||||
expose :merge_ongoing?, as: :merge_ongoing
|
||||
expose :work_in_progress?, as: :work_in_progress
|
||||
|
@ -25,10 +25,10 @@ class MergeRequestPollCachedWidgetEntity < IssuableEntity
|
|||
expose :source_branch_exists?, as: :source_branch_exists
|
||||
expose :branch_missing?, as: :branch_missing
|
||||
|
||||
expose :commits_without_merge_commits, using: MergeRequestWidgetCommitEntity,
|
||||
if: -> (merge_request, _) { merge_request.mergeable? } do |merge_request|
|
||||
expose :commits_without_merge_commits, using: MergeRequestWidgetCommitEntity do |merge_request|
|
||||
merge_request.recent_commits.without_merge_commits
|
||||
end
|
||||
|
||||
expose :diff_head_sha do |merge_request|
|
||||
merge_request.diff_head_sha.presence
|
||||
end
|
||||
|
|
|
@ -47,6 +47,7 @@ class PipelineSerializer < BaseSerializer
|
|||
:retryable_builds,
|
||||
:scheduled_actions,
|
||||
:stages,
|
||||
:latest_statuses,
|
||||
:trigger_requests,
|
||||
:user,
|
||||
{
|
||||
|
@ -62,7 +63,14 @@ class PipelineSerializer < BaseSerializer
|
|||
pending_builds: :project,
|
||||
project: [:route, { namespace: :route }],
|
||||
triggered_by_pipeline: [{ project: [:route, { namespace: :route }] }, :user],
|
||||
triggered_pipelines: [{ project: [:route, { namespace: :route }] }, :user, :source_job]
|
||||
triggered_pipelines: [
|
||||
{
|
||||
project: [:route, { namespace: :route }]
|
||||
},
|
||||
:source_job,
|
||||
:latest_statuses,
|
||||
:user
|
||||
]
|
||||
}
|
||||
]
|
||||
end
|
||||
|
|
|
@ -31,14 +31,14 @@ module Ci
|
|||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def update_retried
|
||||
# find the latest builds for each name
|
||||
latest_statuses = pipeline.statuses.latest
|
||||
latest_statuses = pipeline.latest_statuses
|
||||
.group(:name)
|
||||
.having('count(*) > 1')
|
||||
.pluck(Arel.sql('MAX(id)'), 'name')
|
||||
|
||||
# mark builds that are retried
|
||||
if latest_statuses.any?
|
||||
pipeline.statuses.latest
|
||||
pipeline.latest_statuses
|
||||
.where(name: latest_statuses.map(&:second))
|
||||
.where.not(id: latest_statuses.map(&:first))
|
||||
.update_all(retried: true)
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
- discussion = local_assigns.fetch(:discussion, nil)
|
||||
- if current_user
|
||||
%jump-to-discussion{ "inline-template" => true, ":discussion-id" => "'#{discussion.try(:id)}'" }
|
||||
.btn-group{ role: "group", "v-show" => "!allResolved", "v-if" => "showButton" }
|
||||
%button.btn.gl-button.btn-default.discussion-next-btn.has-tooltip{ "@click" => "jumpToNextUnresolvedDiscussion",
|
||||
":title" => "buttonText",
|
||||
":aria-label" => "buttonText",
|
||||
data: { container: "body" } }
|
||||
= custom_icon("next_discussion")
|
|
@ -1,8 +0,0 @@
|
|||
- if merge_request.discussions_can_be_resolved_by?(current_user) && can?(current_user, :create_issue, @project)
|
||||
.btn-group{ role: "group", "v-if" => "unresolvedDiscussionCount > 0" }
|
||||
= link_to custom_icon('icon_mr_issue'),
|
||||
new_project_issue_path(@project, merge_request_to_resolve_discussions_of: merge_request.iid),
|
||||
title: 'Resolve all discussions in new issue',
|
||||
aria: { label: 'Resolve all discussions in new issue' },
|
||||
data: { container: 'body' },
|
||||
class: 'new-issue-for-discussion btn gl-button btn-default discussion-create-issue-btn has-tooltip'
|
|
@ -1,10 +0,0 @@
|
|||
- if discussion.can_resolve?(current_user) && can?(current_user, :create_issue, @project)
|
||||
%new-issue-for-discussion-btn{ ":discussion-id" => "'#{discussion.id}'",
|
||||
"inline-template" => true }
|
||||
.btn-group{ role: "group", "v-if" => "showButton" }
|
||||
= link_to custom_icon('icon_mr_issue'),
|
||||
new_project_issue_path(@project, merge_request_to_resolve_discussions_of: merge_request.iid, discussion_to_resolve: discussion.id),
|
||||
title: 'Resolve this thread in a new issue',
|
||||
aria: { label: 'Resolve this thread in a new issue' },
|
||||
data: { container: 'body' },
|
||||
class: 'new-issue-for-discussion btn gl-button btn-default discussion-create-issue-btn has-tooltip'
|
|
@ -21,22 +21,8 @@
|
|||
- if can_create_note?
|
||||
%a.user-avatar-link.d-none.d-sm-block{ href: user_path(current_user) }
|
||||
= image_tag avatar_icon_for_user(current_user), alt: current_user.to_reference, class: 'avatar s40'
|
||||
- if discussion.potentially_resolvable?
|
||||
- line_type = local_assigns.fetch(:line_type, nil)
|
||||
|
||||
.discussion-with-resolve-btn
|
||||
.btn-group.discussion-with-resolve-btn{ role: "group" }
|
||||
.btn-group{ role: "group" }
|
||||
= link_to_reply_discussion(discussion, line_type)
|
||||
|
||||
= render "discussions/resolve_all", discussion: discussion
|
||||
|
||||
.btn-group.discussion-actions
|
||||
= render "discussions/new_issue_for_discussion", discussion: discussion, merge_request: discussion.noteable
|
||||
= render "discussions/jump_to_next", discussion: discussion
|
||||
- else
|
||||
.discussion-with-resolve-btn
|
||||
= link_to_reply_discussion(discussion)
|
||||
.discussion-with-resolve-btn
|
||||
= link_to_reply_discussion(discussion)
|
||||
- elsif !current_user
|
||||
.disabled-comment.text-center
|
||||
Please
|
||||
|
|
|
@ -55,12 +55,13 @@
|
|||
- unless use_startup_css?
|
||||
= stylesheet_link_tag_defer "themes/#{user_application_theme_css_filename}" if user_application_theme_css_filename
|
||||
= stylesheet_link_tag "disable_animations", media: "all" if Rails.env.test? || Gitlab.config.gitlab['disable_animations']
|
||||
= stylesheet_link_tag_defer 'performance_bar' if performance_bar_enabled?
|
||||
|
||||
= stylesheet_link_tag_defer "highlight/themes/#{user_color_scheme}"
|
||||
|
||||
= render 'layouts/startup_css_activation'
|
||||
|
||||
= stylesheet_link_tag 'performance_bar' if performance_bar_enabled?
|
||||
|
||||
= Gon::Base.render_data(nonce: content_security_policy_nonce)
|
||||
|
||||
- if content_for?(:library_javascripts)
|
||||
|
|
|
@ -46,4 +46,4 @@
|
|||
%td{ style: "font-family: 'Menlo','Liberation Mono','Consolas','DejaVu Sans Mono','Ubuntu Mono','Courier New','andale mono','lucida console',monospace; font-size: 14px; line-height: 1.4; vertical-align: baseline; padding:0 8px;" }
|
||||
API
|
||||
|
||||
= render 'notify/failed_builds', pipeline: @pipeline, failed: @pipeline.statuses.latest.failed
|
||||
= render 'notify/failed_builds', pipeline: @pipeline, failed: @pipeline.latest_statuses.failed
|
||||
|
|
|
@ -7,7 +7,7 @@ The Auto DevOps pipeline failed for pipeline <%= @pipeline.iid %> (<%= pipeline_
|
|||
<% else -%>
|
||||
Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by API
|
||||
<% end -%>
|
||||
<% failed = @pipeline.statuses.latest.failed -%>
|
||||
<% failed = @pipeline.latest_statuses.failed -%>
|
||||
had <%= failed.size %> failed <%= 'build'.pluralize(failed.size) %>.
|
||||
|
||||
<% failed.each do |build| -%>
|
||||
|
|
|
@ -108,4 +108,4 @@
|
|||
%td{ style: "font-family:'Menlo','Liberation Mono','Consolas','DejaVu Sans Mono','Ubuntu Mono','Courier New','andale mono','lucida console',monospace;font-size:14px;line-height:1.4;vertical-align:baseline;padding:0 5px;" }
|
||||
API
|
||||
|
||||
= render 'notify/failed_builds', pipeline: @pipeline, failed: @pipeline.statuses.latest.failed
|
||||
= render 'notify/failed_builds', pipeline: @pipeline, failed: @pipeline.latest_statuses.failed
|
||||
|
|
|
@ -27,7 +27,7 @@ Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by <%
|
|||
<% else -%>
|
||||
Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by API
|
||||
<% end -%>
|
||||
<% failed = @pipeline.statuses.latest.failed -%>
|
||||
<% failed = @pipeline.latest_statuses.failed -%>
|
||||
had <%= failed.size %> failed <%= 'build'.pluralize(failed.size) %>.
|
||||
|
||||
<% failed.each do |build| -%>
|
||||
|
|
|
@ -21,9 +21,6 @@
|
|||
- else
|
||||
= add_diff_note_button(line_code, diff_file.position(line), type)
|
||||
%a{ href: "##{line_code}", data: { linenumber: link_text } }
|
||||
- discussion = line_discussions.try(:first)
|
||||
- if discussion && discussion.resolvable? && !plain
|
||||
%diff-note-avatars{ "discussion-id" => discussion.id }
|
||||
%td.new_line.diff-line-num{ class: type, data: { linenumber: line.new_pos } }
|
||||
- link_text = type == "old" ? " " : line.new_pos
|
||||
- if plain
|
||||
|
|
|
@ -20,9 +20,6 @@
|
|||
%td.old_line.diff-line-num.js-avatar-container{ class: left.type, data: { linenumber: left.old_pos } }
|
||||
= add_diff_note_button(left_line_code, left_position, 'old')
|
||||
%a{ href: "##{left_line_code}", data: { linenumber: left.old_pos } }
|
||||
- discussion_left = discussions_left.try(:first)
|
||||
- if discussion_left && discussion_left.resolvable?
|
||||
%diff-note-avatars{ "discussion-id" => discussion_left.id }
|
||||
%td.line_content.parallel.left-side{ id: left_line_code, class: left.type }= diff_line_content(left.rich_text)
|
||||
- else
|
||||
%td.old_line.diff-line-num.empty-cell
|
||||
|
@ -41,9 +38,6 @@
|
|||
%td.new_line.diff-line-num.js-avatar-container{ class: right.type, data: { linenumber: right.new_pos } }
|
||||
= add_diff_note_button(right_line_code, right_position, 'new')
|
||||
%a{ href: "##{right_line_code}", data: { linenumber: right.new_pos } }
|
||||
- discussion_right = discussions_right.try(:first)
|
||||
- if discussion_right && discussion_right.resolvable?
|
||||
%diff-note-avatars{ "discussion-id" => discussion_right.id }
|
||||
%td.line_content.parallel.right-side{ id: right_line_code, class: right.type }= diff_line_content(right.rich_text)
|
||||
- else
|
||||
%td.old_line.diff-line-num.empty-cell
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
- content_for :note_actions do
|
||||
- if can?(current_user, :update_merge_request, @merge_request)
|
||||
- if @merge_request.open?
|
||||
= link_to 'Close merge request', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, class: "btn btn-nr btn-comment btn-close close-mr-link js-note-target-close", title: "Close merge request", data: { original_text: "Close merge request", alternative_text: "Comment & close merge request"}
|
||||
- if @merge_request.reopenable?
|
||||
= link_to 'Reopen merge request', merge_request_path(@merge_request, merge_request: { state_event: :reopen }), method: :put, class: "btn btn-nr btn-comment btn-reopen reopen-mr-link js-note-target-close js-note-target-reopen", title: "Reopen merge request", data: { original_text: "Reopen merge request", alternative_text: "Comment & reopen merge request"}
|
||||
%comment-and-resolve-btn{ "inline-template" => true }
|
||||
%button.btn.btn-nr.btn-default.gl-mr-3.js-comment-resolve-button{ "v-if" => "showButton", type: "submit", data: { project_path: "#{project_path(@merge_request.project)}" } }
|
||||
{{ buttonText }}
|
||||
|
||||
#notes= render "shared/notes/notes_with_form", :autocomplete => true
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
- page_title _("Pipeline Schedules")
|
||||
|
||||
#pipeline-schedules-callout{ data: { docs_url: help_page_path('ci/pipelines/schedules') } }
|
||||
#pipeline-schedules-callout{ data: { docs_url: help_page_path('ci/pipelines/schedules'), image_url: image_path('pages/projects/pipeline_schedules/shared/icons/intro_illustration.svg') } }
|
||||
.top-area
|
||||
- schedule_path_proc = ->(scope) { pipeline_schedules_path(@project, scope: scope) }
|
||||
= render "tabs", schedule_path_proc: schedule_path_proc, all_schedules: @all_schedules, scope: @scope
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
<svg viewBox="0 0 20 19" ><path d="M15.21 7.783h-3.317c-.268 0-.472.218-.472.486v.953c0 .28.212.486.473.486h3.318v1.575c0 .36.233.452.52.23l3.06-2.37c.274-.213.286-.582 0-.804l-3.06-2.37c-.275-.213-.52-.12-.52.23v1.583zm.57-3.66c-1.558-1.22-3.783-1.98-6.254-1.98C4.816 2.143 1 4.91 1 8.333c0 1.964 1.256 3.715 3.216 4.846-.447 1.615-1.132 2.195-1.732 2.882-.142.174-.304.32-.256.56v.01c.047.213.218.368.41.368h.046c.37-.048.743-.116 1.085-.213 1.645-.425 3.13-1.22 4.377-2.34.447.048.913.077 1.38.077 2.092 0 4.01-.546 5.492-1.454-.416-.208-.798-.475-1.134-.792-1.227.63-2.743 1.008-4.36 1.008-.41 0-.828-.03-1.237-.078l-.543-.058-.41.368c-.78.696-1.655 1.248-2.616 1.654.248-.445.486-.977.667-1.664l.257-.928-.828-.484c-1.646-.948-2.598-2.32-2.598-3.763 0-2.69 3.35-4.952 7.308-4.952 1.893 0 3.647.518 4.962 1.353.393-.266.827-.473 1.29-.61z" /></svg>
|
Before Width: | Height: | Size: 853 B |
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add confirmation modal on instance-level integration form
|
||||
merge_request: 42840
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add markdown icon to more file extensions
|
||||
merge_request: 43479
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add Debian API skeleton
|
||||
merge_request: 42670
|
||||
author: Mathieu Parent
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix suggested squashed messages for MR
|
||||
merge_request: 43508
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Improve n+1 in pipeline serializer for triggered pipelines
|
||||
merge_request: 42421
|
||||
author:
|
||||
type: performance
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
title: Color/position tweaks for collapsed diff files
|
||||
merge_request: 42465
|
||||
author:
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: 'Revert 42465 and 42343: Expanded collapsed diff files'
|
||||
merge_request: 43361
|
||||
author:
|
||||
type: other
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
name: debian_packages
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/42670
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/5835
|
||||
group: group::package
|
||||
type: development
|
||||
default_enabled: false
|
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddDebianMaxFileSizeToPlanLimits < ActiveRecord::Migration[6.0]
|
||||
DOWNTIME = false
|
||||
|
||||
def change
|
||||
add_column :plan_limits, :debian_max_file_size, :bigint, default: 3.gigabytes, null: false
|
||||
end
|
||||
end
|
|
@ -0,0 +1 @@
|
|||
18a3981a3becefe6700dd5fea87e8ba9478c0e83ddc80de1b3ee2ed77c221ce6
|
|
@ -14365,7 +14365,8 @@ CREATE TABLE plan_limits (
|
|||
nuget_max_file_size bigint DEFAULT 524288000 NOT NULL,
|
||||
pypi_max_file_size bigint DEFAULT '3221225472'::bigint NOT NULL,
|
||||
generic_packages_max_file_size bigint DEFAULT '5368709120'::bigint NOT NULL,
|
||||
golang_max_file_size bigint DEFAULT 104857600 NOT NULL
|
||||
golang_max_file_size bigint DEFAULT 104857600 NOT NULL,
|
||||
debian_max_file_size bigint DEFAULT '3221225472'::bigint NOT NULL
|
||||
);
|
||||
|
||||
CREATE SEQUENCE plan_limits_id_seq
|
||||
|
|
|
@ -552,6 +552,9 @@ Plan.default.actual_limits.update!(maven_max_file_size: 100.megabytes)
|
|||
|
||||
# For PyPI Packages
|
||||
Plan.default.actual_limits.update!(pypi_max_file_size: 100.megabytes)
|
||||
|
||||
# For Debian Packages
|
||||
Plan.default.actual_limits.update!(debian_max_file_size: 100.megabytes)
|
||||
```
|
||||
|
||||
Set the limit to `0` to allow any file size.
|
||||
|
|
|
@ -13714,7 +13714,7 @@ type Project {
|
|||
requestAccessEnabled: Boolean
|
||||
|
||||
"""
|
||||
Find a single requirement. Available only when feature flag `requirements_management` is enabled.
|
||||
Find a single requirement
|
||||
"""
|
||||
requirement(
|
||||
"""
|
||||
|
@ -13754,7 +13754,7 @@ type Project {
|
|||
requirementStatesCount: RequirementStatesCount
|
||||
|
||||
"""
|
||||
Find requirements. Available only when feature flag `requirements_management` is enabled.
|
||||
Find requirements
|
||||
"""
|
||||
requirements(
|
||||
"""
|
||||
|
|
|
@ -39901,7 +39901,7 @@
|
|||
},
|
||||
{
|
||||
"name": "requirement",
|
||||
"description": "Find a single requirement. Available only when feature flag `requirements_management` is enabled.",
|
||||
"description": "Find a single requirement",
|
||||
"args": [
|
||||
{
|
||||
"name": "iid",
|
||||
|
@ -40004,7 +40004,7 @@
|
|||
},
|
||||
{
|
||||
"name": "requirements",
|
||||
"description": "Find requirements. Available only when feature flag `requirements_management` is enabled.",
|
||||
"description": "Find requirements",
|
||||
"args": [
|
||||
{
|
||||
"name": "iid",
|
||||
|
|
|
@ -1899,7 +1899,7 @@ Autogenerated return type of PipelineRetry.
|
|||
| `removeSourceBranchAfterMerge` | Boolean | Indicates if `Delete source branch` option should be enabled by default for all new merge requests of the project |
|
||||
| `repository` | Repository | Git repository of the project |
|
||||
| `requestAccessEnabled` | Boolean | Indicates if users can request member access to the project |
|
||||
| `requirement` | Requirement | Find a single requirement. Available only when feature flag `requirements_management` is enabled. |
|
||||
| `requirement` | Requirement | Find a single requirement |
|
||||
| `requirementStatesCount` | RequirementStatesCount | Number of requirements for the project by their state |
|
||||
| `sastCiConfiguration` | SastCiConfiguration | SAST CI configuration for the project |
|
||||
| `securityDashboardPath` | String | Path to project's security dashboard |
|
||||
|
|
|
@ -75,7 +75,7 @@ For information about setting a maximum artifact size for a project, see
|
|||
> - [Support for external `.gitlab-ci.yml` locations](https://gitlab.com/gitlab-org/gitlab/-/issues/14376) introduced in GitLab 12.6.
|
||||
|
||||
By default we look for the `.gitlab-ci.yml` file in the project's root
|
||||
directory. If needed, you can specify an alternate path and file name, including locations outside the project.
|
||||
directory. If needed, you can specify an alternate path and filename, including locations outside the project.
|
||||
|
||||
To customize the path:
|
||||
|
||||
|
@ -297,7 +297,7 @@ into your `README.md`:
|
|||
|
||||
### Badge styles
|
||||
|
||||
Pipeline badges can be rendered in different styles by adding the `style=style_name` parameter to the URL. Currently two styles are available:
|
||||
Pipeline badges can be rendered in different styles by adding the `style=style_name` parameter to the URL. Two styles are available:
|
||||
|
||||
#### Flat (default)
|
||||
|
||||
|
|
|
@ -196,6 +196,8 @@ module API
|
|||
mount ::API::ComposerPackages
|
||||
mount ::API::ConanProjectPackages
|
||||
mount ::API::ConanInstancePackages
|
||||
mount ::API::DebianGroupPackages
|
||||
mount ::API::DebianProjectPackages
|
||||
mount ::API::MavenPackages
|
||||
mount ::API::NpmPackages
|
||||
mount ::API::GenericPackages
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module API
|
||||
class DebianGroupPackages < Grape::API::Instance
|
||||
params do
|
||||
requires :id, type: String, desc: 'The ID of a group'
|
||||
end
|
||||
|
||||
resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
|
||||
before do
|
||||
not_found! unless ::Feature.enabled?(:debian_packages, user_group)
|
||||
|
||||
authorize_read_package!(user_group)
|
||||
end
|
||||
|
||||
namespace ':id/-/packages/debian' do
|
||||
include DebianPackageEndpoints
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,124 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module API
|
||||
module DebianPackageEndpoints
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
DISTRIBUTION_REGEX = %r{[a-zA-Z0-9][a-zA-Z0-9.-]*}.freeze
|
||||
COMPONENT_REGEX = %r{[a-z-]+}.freeze
|
||||
ARCHITECTURE_REGEX = %r{[a-z][a-z0-9]*}.freeze
|
||||
LETTER_REGEX = %r{(lib)?[a-z0-9]}.freeze
|
||||
PACKAGE_REGEX = API::NO_SLASH_URL_PART_REGEX
|
||||
DISTRIBUTION_REQUIREMENTS = {
|
||||
distribution: DISTRIBUTION_REGEX
|
||||
}.freeze
|
||||
COMPONENT_ARCHITECTURE_REQUIREMENTS = {
|
||||
component: COMPONENT_REGEX,
|
||||
architecture: ARCHITECTURE_REGEX
|
||||
}.freeze
|
||||
COMPONENT_LETTER_SOURCE_PACKAGE_REQUIREMENTS = {
|
||||
component: COMPONENT_REGEX,
|
||||
letter: LETTER_REGEX,
|
||||
source_package: PACKAGE_REGEX
|
||||
}.freeze
|
||||
FILE_NAME_REQUIREMENTS = {
|
||||
file_name: API::NO_SLASH_URL_PART_REGEX
|
||||
}.freeze
|
||||
|
||||
included do
|
||||
helpers ::API::Helpers::PackagesHelpers
|
||||
helpers ::API::Helpers::Packages::BasicAuthHelpers
|
||||
|
||||
format :txt
|
||||
|
||||
rescue_from ArgumentError do |e|
|
||||
render_api_error!(e.message, 400)
|
||||
end
|
||||
|
||||
rescue_from ActiveRecord::RecordInvalid do |e|
|
||||
render_api_error!(e.message, 400)
|
||||
end
|
||||
|
||||
before do
|
||||
require_packages_enabled!
|
||||
end
|
||||
|
||||
params do
|
||||
requires :distribution, type: String, desc: 'The Debian Codename', file_path: true
|
||||
end
|
||||
|
||||
namespace 'dists/*distribution', requirements: DISTRIBUTION_REQUIREMENTS do
|
||||
# GET {projects|groups}/:id/-/packages/debian/dists/*distribution/Release.gpg
|
||||
desc 'The Release file signature' do
|
||||
detail 'This feature was introduced in GitLab 13.5'
|
||||
end
|
||||
|
||||
route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth
|
||||
get 'Release.gpg' do
|
||||
not_found!
|
||||
end
|
||||
|
||||
# GET {projects|groups}/:id/-/packages/debian/dists/*distribution/Release
|
||||
desc 'The unsigned Release file' do
|
||||
detail 'This feature was introduced in GitLab 13.5'
|
||||
end
|
||||
|
||||
route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth
|
||||
get 'Release' do
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/5835#note_414103286
|
||||
'TODO Release'
|
||||
end
|
||||
|
||||
# GET {projects|groups}/:id/-/packages/debian/dists/*distribution/InRelease
|
||||
desc 'The signed Release file' do
|
||||
detail 'This feature was introduced in GitLab 13.5'
|
||||
end
|
||||
|
||||
route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth
|
||||
get 'InRelease' do
|
||||
not_found!
|
||||
end
|
||||
|
||||
params do
|
||||
requires :component, type: String, desc: 'The Debian Component'
|
||||
requires :architecture, type: String, desc: 'The Debian Architecture'
|
||||
end
|
||||
|
||||
namespace ':component/binary-:architecture', requirements: COMPONENT_ARCHITECTURE_REQUIREMENTS do
|
||||
# GET {projects|groups}/:id/-/packages/debian/dists/*distribution/:component/binary-:architecture/Packages
|
||||
desc 'The binary files index' do
|
||||
detail 'This feature was introduced in GitLab 13.5'
|
||||
end
|
||||
|
||||
route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth
|
||||
get 'Packages' do
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/5835#note_414103286
|
||||
'TODO Packages'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
params do
|
||||
requires :component, type: String, desc: 'The Debian Component'
|
||||
requires :letter, type: String, desc: 'The Debian Classification (first-letter or lib-first-letter)'
|
||||
requires :source_package, type: String, desc: 'The Debian Source Package Name'
|
||||
end
|
||||
|
||||
namespace 'pool/:component/:letter/:source_package', requirements: COMPONENT_LETTER_SOURCE_PACKAGE_REQUIREMENTS do
|
||||
# GET {projects|groups}/:id/-/packages/debian/pool/:component/:letter/:source_package/:file_name
|
||||
params do
|
||||
requires :file_name, type: String, desc: 'The Debian File Name'
|
||||
end
|
||||
desc 'The package' do
|
||||
detail 'This feature was introduced in GitLab 13.5'
|
||||
end
|
||||
|
||||
route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth
|
||||
get ':file_name', requirements: FILE_NAME_REQUIREMENTS do
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/5835#note_414103286
|
||||
'TODO File'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,56 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module API
|
||||
class DebianProjectPackages < Grape::API::Instance
|
||||
params do
|
||||
requires :id, type: String, desc: 'The ID of a project'
|
||||
end
|
||||
|
||||
resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
|
||||
before do
|
||||
not_found! unless ::Feature.enabled?(:debian_packages, user_project)
|
||||
|
||||
authorize_read_package!
|
||||
end
|
||||
|
||||
namespace ':id/-/packages/debian' do
|
||||
include DebianPackageEndpoints
|
||||
|
||||
params do
|
||||
requires :file_name, type: String, desc: 'The file name'
|
||||
end
|
||||
|
||||
namespace 'incoming/:file_name', requirements: FILE_NAME_REQUIREMENTS do
|
||||
# PUT {projects|groups}/:id/-/packages/debian/incoming/:file_name
|
||||
params do
|
||||
requires :file, type: ::API::Validations::Types::WorkhorseFile, desc: 'The package file to be published (generated by Multipart middleware)'
|
||||
end
|
||||
|
||||
route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth
|
||||
put do
|
||||
authorize_upload!(authorized_user_project)
|
||||
bad_request!('File is too large') if authorized_user_project.actual_limits.exceeded?(:debian_max_file_size, params[:file].size)
|
||||
|
||||
package_event('push_package')
|
||||
|
||||
created!
|
||||
rescue ObjectStorage::RemoteStoreError => e
|
||||
Gitlab::ErrorTracking.track_exception(e, extra: { file_name: params[:file_name], project_id: authorized_user_project.id })
|
||||
|
||||
forbidden!
|
||||
end
|
||||
|
||||
# PUT {projects|groups}/:id/-/packages/debian/incoming/:file_name/authorize
|
||||
route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth
|
||||
post 'authorize' do
|
||||
authorize_workhorse!(
|
||||
subject: authorized_user_project,
|
||||
has_length: false,
|
||||
maximum_size: authorized_user_project.actual_limits.debian_max_file_size
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -2675,12 +2675,6 @@ msgstr ""
|
|||
msgid "An error occurred when toggling the notification subscription"
|
||||
msgstr ""
|
||||
|
||||
msgid "An error occurred when trying to resolve a comment. Please try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "An error occurred when trying to resolve a discussion. Please try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "An error occurred when updating the issue weight"
|
||||
msgstr ""
|
||||
|
||||
|
@ -10458,9 +10452,6 @@ msgstr ""
|
|||
msgid "Expand approvers"
|
||||
msgstr ""
|
||||
|
||||
msgid "Expand file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Expand milestones"
|
||||
msgstr ""
|
||||
|
||||
|
@ -13770,6 +13761,15 @@ msgstr ""
|
|||
msgid "Integrations|Includes commit title and branch"
|
||||
msgstr ""
|
||||
|
||||
msgid "Integrations|Projects using custom settings will not be impacted unless the project owner chooses to use instance-level defaults."
|
||||
msgstr ""
|
||||
|
||||
msgid "Integrations|Save settings?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Integrations|Saving will update the default settings for all projects that are not using custom settings."
|
||||
msgstr ""
|
||||
|
||||
msgid "Integrations|Standard"
|
||||
msgstr ""
|
||||
|
||||
|
@ -14451,9 +14451,6 @@ msgstr ""
|
|||
msgid "July"
|
||||
msgstr ""
|
||||
|
||||
msgid "Jump to first unresolved thread"
|
||||
msgstr ""
|
||||
|
||||
msgid "Jump to next unresolved thread"
|
||||
msgstr ""
|
||||
|
||||
|
@ -21760,9 +21757,6 @@ msgstr ""
|
|||
msgid "Resolved by %{name}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Resolved by %{resolvedByName}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Resolves IP addresses once and uses them to submit requests"
|
||||
msgstr ""
|
||||
|
||||
|
@ -26026,9 +26020,6 @@ msgstr ""
|
|||
msgid "This field is required."
|
||||
msgstr ""
|
||||
|
||||
msgid "This file is collapsed."
|
||||
msgstr ""
|
||||
|
||||
msgid "This group"
|
||||
msgstr ""
|
||||
|
||||
|
@ -27248,9 +27239,6 @@ msgstr ""
|
|||
msgid "Unable to load the merge request widget. Try reloading the page."
|
||||
msgstr ""
|
||||
|
||||
msgid "Unable to resolve"
|
||||
msgstr ""
|
||||
|
||||
msgid "Unable to save iteration. Please try again"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -164,7 +164,7 @@ module QA
|
|||
|
||||
def has_pipeline_status?(text)
|
||||
# Pipelines can be slow, so we wait a bit longer than the usual 10 seconds
|
||||
has_element?(:merge_request_pipeline_info_content, text: text, wait: 30)
|
||||
has_element?(:merge_request_pipeline_info_content, text: text, wait: 60)
|
||||
end
|
||||
|
||||
def has_title?(title)
|
||||
|
@ -198,7 +198,7 @@ module QA
|
|||
end
|
||||
|
||||
def merged?
|
||||
has_element?(:merged_status_content, text: 'The changes were merged into', wait: 30)
|
||||
has_element?(:merged_status_content, text: 'The changes were merged into', wait: 60)
|
||||
end
|
||||
|
||||
# Check if the MR is able to be merged
|
||||
|
|
|
@ -16,7 +16,7 @@ module QA
|
|||
project.initialize_with_readme = true
|
||||
end
|
||||
|
||||
it 'sets labels' do
|
||||
it 'sets labels', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1032' do
|
||||
Resource::Repository::ProjectPush.fabricate! do |push|
|
||||
push.project = project
|
||||
push.commit_message = commit_message
|
||||
|
@ -35,7 +35,7 @@ module QA
|
|||
end
|
||||
|
||||
context 'when labels are set already' do
|
||||
it 'removes them' do
|
||||
it 'removes them', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1033' do
|
||||
Resource::Repository::ProjectPush.fabricate! do |push|
|
||||
push.project = project
|
||||
push.file_content = "Unlabel test #{SecureRandom.hex(8)}"
|
||||
|
|
|
@ -29,7 +29,7 @@ module QA
|
|||
runner.remove_via_api!
|
||||
end
|
||||
|
||||
it 'sets merge when pipeline succeeds' do
|
||||
it 'sets merge when pipeline succeeds', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1037' do
|
||||
Resource::Repository::Commit.fabricate_via_api! do |commit|
|
||||
commit.project = project
|
||||
commit.commit_message = 'Add .gitlab-ci.yml'
|
||||
|
@ -72,7 +72,7 @@ module QA
|
|||
expect(merge_request.merge_when_pipeline_succeeds).to be true
|
||||
end
|
||||
|
||||
it 'merges when pipeline succeeds' do
|
||||
it 'merges when pipeline succeeds', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1036' do
|
||||
Resource::Repository::Commit.fabricate_via_api! do |commit|
|
||||
commit.project = project
|
||||
commit.commit_message = 'Add .gitlab-ci.yml'
|
||||
|
|
|
@ -17,7 +17,7 @@ module QA
|
|||
end
|
||||
end
|
||||
|
||||
it 'removes the source branch' do
|
||||
it 'removes the source branch', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1035' do
|
||||
Resource::Repository::ProjectPush.fabricate! do |push|
|
||||
push.project = project
|
||||
push.branch_name = branch
|
||||
|
|
|
@ -16,7 +16,7 @@ module QA
|
|||
end
|
||||
end
|
||||
|
||||
it 'sets a target branch' do
|
||||
it 'sets a target branch', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1034' do
|
||||
target_branch = "push-options-test-target-#{SecureRandom.hex(8)}"
|
||||
|
||||
Resource::Repository::ProjectPush.fabricate! do |push|
|
||||
|
|
|
@ -14,7 +14,7 @@ module QA
|
|||
end
|
||||
end
|
||||
|
||||
it 'sets title and description' do
|
||||
it 'sets title and description', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1038' do
|
||||
description = "This is a test of MR push options"
|
||||
title = "MR push options test #{SecureRandom.hex(8)}"
|
||||
|
||||
|
|
|
@ -29,6 +29,43 @@ RSpec.describe InvitesController, :snowplow do
|
|||
end
|
||||
end
|
||||
|
||||
shared_examples "tracks the 'accepted' event for the invitation reminders experiment" do
|
||||
before do
|
||||
stub_experiment(invitation_reminders: true)
|
||||
allow(Gitlab::Experimentation).to receive(:enabled_for_attribute?).with(:invitation_reminders, member.invite_email).and_return(experimental_group)
|
||||
end
|
||||
|
||||
context 'when in the control group' do
|
||||
let(:experimental_group) { false }
|
||||
|
||||
it "tracks the 'accepted' event" do
|
||||
request
|
||||
|
||||
expect_snowplow_event(
|
||||
category: 'Growth::Acquisition::Experiment::InvitationReminders',
|
||||
label: md5_member_global_id,
|
||||
property: 'control_group',
|
||||
action: 'accepted'
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when in the experimental group' do
|
||||
let(:experimental_group) { true }
|
||||
|
||||
it "tracks the 'accepted' event" do
|
||||
request
|
||||
|
||||
expect_snowplow_event(
|
||||
category: 'Growth::Acquisition::Experiment::InvitationReminders',
|
||||
label: md5_member_global_id,
|
||||
property: 'experimental_group',
|
||||
action: 'accepted'
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET #show' do
|
||||
subject(:request) { get :show, params: params }
|
||||
|
||||
|
@ -89,6 +126,7 @@ RSpec.describe InvitesController, :snowplow do
|
|||
end
|
||||
end
|
||||
|
||||
it_behaves_like "tracks the 'accepted' event for the invitation reminders experiment"
|
||||
it_behaves_like 'invalid token'
|
||||
end
|
||||
|
||||
|
@ -150,6 +188,7 @@ RSpec.describe InvitesController, :snowplow do
|
|||
end
|
||||
end
|
||||
|
||||
it_behaves_like "tracks the 'accepted' event for the invitation reminders experiment"
|
||||
it_behaves_like 'invalid token'
|
||||
end
|
||||
|
||||
|
|
|
@ -15,11 +15,11 @@ RSpec.describe 'User expands diff', :js do
|
|||
|
||||
it 'allows user to expand diff' do
|
||||
page.within find('[id="19763941ab80e8c09871c0a425f0560d9053bcb3"]') do
|
||||
find('[data-testid="expandButton"]').click
|
||||
click_link 'Click to expand it.'
|
||||
|
||||
wait_for_requests
|
||||
|
||||
expect(page).not_to have_content('Expand File')
|
||||
expect(page).not_to have_content('Click to expand it.')
|
||||
expect(page).to have_selector('.code')
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
empty
|
|
@ -1,136 +0,0 @@
|
|||
/* global CommentsStore */
|
||||
|
||||
import '~/diff_notes/models/discussion';
|
||||
import '~/diff_notes/models/note';
|
||||
import '~/diff_notes/stores/comments';
|
||||
|
||||
function createDiscussion(noteId = 1, resolved = true) {
|
||||
CommentsStore.create({
|
||||
discussionId: 'a',
|
||||
noteId,
|
||||
canResolve: true,
|
||||
resolved,
|
||||
resolvedBy: 'test',
|
||||
authorName: 'test',
|
||||
authorAvatar: 'test',
|
||||
noteTruncated: 'test...',
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
CommentsStore.state = {};
|
||||
});
|
||||
|
||||
describe('New discussion', () => {
|
||||
it('creates new discussion', () => {
|
||||
expect(Object.keys(CommentsStore.state).length).toBe(0);
|
||||
createDiscussion();
|
||||
|
||||
expect(Object.keys(CommentsStore.state).length).toBe(1);
|
||||
});
|
||||
|
||||
it('creates new note in discussion', () => {
|
||||
createDiscussion();
|
||||
createDiscussion(2);
|
||||
|
||||
const discussion = CommentsStore.state.a;
|
||||
|
||||
expect(Object.keys(discussion.notes).length).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Get note', () => {
|
||||
beforeEach(() => {
|
||||
createDiscussion();
|
||||
});
|
||||
|
||||
it('gets note by ID', () => {
|
||||
const note = CommentsStore.get('a', 1);
|
||||
|
||||
expect(note).toBeDefined();
|
||||
expect(note.id).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Delete discussion', () => {
|
||||
beforeEach(() => {
|
||||
createDiscussion();
|
||||
});
|
||||
|
||||
it('deletes discussion by ID', () => {
|
||||
CommentsStore.delete('a', 1);
|
||||
|
||||
expect(Object.keys(CommentsStore.state).length).toBe(0);
|
||||
});
|
||||
|
||||
it('deletes discussion when no more notes', () => {
|
||||
createDiscussion();
|
||||
createDiscussion(2);
|
||||
|
||||
expect(Object.keys(CommentsStore.state).length).toBe(1);
|
||||
expect(Object.keys(CommentsStore.state.a.notes).length).toBe(2);
|
||||
|
||||
CommentsStore.delete('a', 1);
|
||||
CommentsStore.delete('a', 2);
|
||||
|
||||
expect(Object.keys(CommentsStore.state).length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Update note', () => {
|
||||
beforeEach(() => {
|
||||
createDiscussion();
|
||||
});
|
||||
|
||||
it('updates note to be unresolved', () => {
|
||||
CommentsStore.update('a', 1, false, 'test');
|
||||
|
||||
const note = CommentsStore.get('a', 1);
|
||||
|
||||
expect(note.resolved).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Discussion resolved', () => {
|
||||
beforeEach(() => {
|
||||
createDiscussion();
|
||||
});
|
||||
|
||||
it('is resolved with single note', () => {
|
||||
const discussion = CommentsStore.state.a;
|
||||
|
||||
expect(discussion.isResolved()).toBe(true);
|
||||
});
|
||||
|
||||
it('is unresolved with 2 notes', () => {
|
||||
const discussion = CommentsStore.state.a;
|
||||
createDiscussion(2, false);
|
||||
|
||||
expect(discussion.isResolved()).toBe(false);
|
||||
});
|
||||
|
||||
it('is resolved with 2 notes', () => {
|
||||
const discussion = CommentsStore.state.a;
|
||||
createDiscussion(2);
|
||||
|
||||
expect(discussion.isResolved()).toBe(true);
|
||||
});
|
||||
|
||||
it('resolve all notes', () => {
|
||||
const discussion = CommentsStore.state.a;
|
||||
createDiscussion(2, false);
|
||||
|
||||
discussion.resolveAllNotes();
|
||||
|
||||
expect(discussion.isResolved()).toBe(true);
|
||||
});
|
||||
|
||||
it('unresolve all notes', () => {
|
||||
const discussion = CommentsStore.state.a;
|
||||
createDiscussion(2);
|
||||
|
||||
discussion.unResolveAllNotes();
|
||||
|
||||
expect(discussion.isResolved()).toBe(false);
|
||||
});
|
||||
});
|
|
@ -90,8 +90,8 @@ describe('DiffFile', () => {
|
|||
vm.isCollapsed = true;
|
||||
|
||||
vm.$nextTick(() => {
|
||||
expect(vm.$el.innerText).toContain('This file is collapsed.');
|
||||
expect(vm.$el.querySelector('[data-testid="expandButton"]')).not.toBeFalsy();
|
||||
expect(vm.$el.innerText).toContain('This diff is collapsed');
|
||||
expect(vm.$el.querySelectorAll('.js-click-to-expand').length).toEqual(1);
|
||||
|
||||
done();
|
||||
});
|
||||
|
@ -102,8 +102,8 @@ describe('DiffFile', () => {
|
|||
vm.isCollapsed = true;
|
||||
|
||||
vm.$nextTick(() => {
|
||||
expect(vm.$el.innerText).toContain('This file is collapsed.');
|
||||
expect(vm.$el.querySelector('[data-testid="expandButton"]')).not.toBeFalsy();
|
||||
expect(vm.$el.innerText).toContain('This diff is collapsed');
|
||||
expect(vm.$el.querySelectorAll('.js-click-to-expand').length).toEqual(1);
|
||||
|
||||
done();
|
||||
});
|
||||
|
@ -121,8 +121,8 @@ describe('DiffFile', () => {
|
|||
vm.isCollapsed = true;
|
||||
|
||||
vm.$nextTick(() => {
|
||||
expect(vm.$el.innerText).toContain('This file is collapsed.');
|
||||
expect(vm.$el.querySelector('[data-testid="expandButton"]')).not.toBeFalsy();
|
||||
expect(vm.$el.innerText).toContain('This diff is collapsed');
|
||||
expect(vm.$el.querySelectorAll('.js-click-to-expand').length).toEqual(1);
|
||||
|
||||
done();
|
||||
});
|
||||
|
@ -135,7 +135,7 @@ describe('DiffFile', () => {
|
|||
vm.file.viewer.name = diffViewerModes.renamed;
|
||||
|
||||
vm.$nextTick(() => {
|
||||
expect(vm.$el.innerText).not.toContain('This file is collapsed.');
|
||||
expect(vm.$el.innerText).not.toContain('This diff is collapsed');
|
||||
|
||||
done();
|
||||
});
|
||||
|
@ -148,7 +148,7 @@ describe('DiffFile', () => {
|
|||
vm.file.viewer.name = diffViewerModes.mode_changed;
|
||||
|
||||
vm.$nextTick(() => {
|
||||
expect(vm.$el.innerText).not.toContain('This file is collapsed.');
|
||||
expect(vm.$el.innerText).not.toContain('This diff is collapsed');
|
||||
|
||||
done();
|
||||
});
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { GlModal } from '@gitlab/ui';
|
||||
import { createStore } from '~/integrations/edit/store';
|
||||
|
||||
import ConfirmationModal from '~/integrations/edit/components/confirmation_modal.vue';
|
||||
|
||||
describe('ConfirmationModal', () => {
|
||||
let wrapper;
|
||||
|
||||
const createComponent = () => {
|
||||
wrapper = shallowMount(ConfirmationModal, {
|
||||
store: createStore(),
|
||||
});
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
if (wrapper) {
|
||||
wrapper.destroy();
|
||||
wrapper = null;
|
||||
}
|
||||
});
|
||||
|
||||
const findGlModal = () => wrapper.find(GlModal);
|
||||
|
||||
describe('template', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('renders GlModal with correct copy', () => {
|
||||
expect(findGlModal().exists()).toBe(true);
|
||||
expect(findGlModal().attributes('title')).toBe('Save settings?');
|
||||
expect(findGlModal().text()).toContain(
|
||||
'Saving will update the default settings for all projects that are not using custom settings.',
|
||||
);
|
||||
expect(findGlModal().text()).toContain(
|
||||
'Projects using custom settings will not be impacted unless the project owner chooses to use instance-level defaults.',
|
||||
);
|
||||
});
|
||||
|
||||
it('emits `submit` event when `primary` event is emitted on GlModal', async () => {
|
||||
expect(wrapper.emitted().submit).toBeUndefined();
|
||||
|
||||
findGlModal().vm.$emit('primary');
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.emitted().submit).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -4,6 +4,7 @@ import { createStore } from '~/integrations/edit/store';
|
|||
import IntegrationForm from '~/integrations/edit/components/integration_form.vue';
|
||||
import OverrideDropdown from '~/integrations/edit/components/override_dropdown.vue';
|
||||
import ActiveCheckbox from '~/integrations/edit/components/active_checkbox.vue';
|
||||
import ConfirmationModal from '~/integrations/edit/components/confirmation_modal.vue';
|
||||
import JiraTriggerFields from '~/integrations/edit/components/jira_trigger_fields.vue';
|
||||
import JiraIssuesFields from '~/integrations/edit/components/jira_issues_fields.vue';
|
||||
import TriggerFields from '~/integrations/edit/components/trigger_fields.vue';
|
||||
|
@ -22,6 +23,7 @@ describe('IntegrationForm', () => {
|
|||
stubs: {
|
||||
OverrideDropdown,
|
||||
ActiveCheckbox,
|
||||
ConfirmationModal,
|
||||
JiraTriggerFields,
|
||||
TriggerFields,
|
||||
},
|
||||
|
@ -40,6 +42,7 @@ describe('IntegrationForm', () => {
|
|||
|
||||
const findOverrideDropdown = () => wrapper.find(OverrideDropdown);
|
||||
const findActiveCheckbox = () => wrapper.find(ActiveCheckbox);
|
||||
const findConfirmationModal = () => wrapper.find(ConfirmationModal);
|
||||
const findJiraTriggerFields = () => wrapper.find(JiraTriggerFields);
|
||||
const findJiraIssuesFields = () => wrapper.find(JiraIssuesFields);
|
||||
const findTriggerFields = () => wrapper.find(TriggerFields);
|
||||
|
@ -63,6 +66,26 @@ describe('IntegrationForm', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('integrationLevel is instance', () => {
|
||||
it('renders ConfirmationModal', () => {
|
||||
createComponent({
|
||||
integrationLevel: 'instance',
|
||||
});
|
||||
|
||||
expect(findConfirmationModal().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('integrationLevel is not instance', () => {
|
||||
it('does not render ConfirmationModal', () => {
|
||||
createComponent({
|
||||
integrationLevel: 'project',
|
||||
});
|
||||
|
||||
expect(findConfirmationModal().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('type is "slack"', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ type: 'slack' });
|
||||
|
|
|
@ -2,6 +2,7 @@ export const mockIntegrationProps = {
|
|||
id: 25,
|
||||
initialActivated: true,
|
||||
showActive: true,
|
||||
editable: true,
|
||||
triggerFieldsProps: {
|
||||
initialTriggerCommit: false,
|
||||
initialTriggerMergeRequest: false,
|
||||
|
|
|
@ -1,23 +1,18 @@
|
|||
import Vue from 'vue';
|
||||
import Cookies from 'js-cookie';
|
||||
import PipelineSchedulesCallout from '~/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue';
|
||||
import '~/pages/projects/pipeline_schedules/shared/icons/intro_illustration.svg';
|
||||
|
||||
jest.mock(
|
||||
'~/pages/projects/pipeline_schedules/shared/icons/intro_illustration.svg',
|
||||
() => '<svg></svg>',
|
||||
);
|
||||
|
||||
const PipelineSchedulesCalloutComponent = Vue.extend(PipelineSchedulesCallout);
|
||||
const cookieKey = 'pipeline_schedules_callout_dismissed';
|
||||
const docsUrl = 'help/ci/scheduled_pipelines';
|
||||
const imageUrl = 'pages/projects/pipeline_schedules/shared/icons/intro_illustration.svg';
|
||||
|
||||
describe('Pipeline Schedule Callout', () => {
|
||||
let calloutComponent;
|
||||
|
||||
beforeEach(() => {
|
||||
setFixtures(`
|
||||
<div id='pipeline-schedules-callout' data-docs-url=${docsUrl}></div>
|
||||
<div id='pipeline-schedules-callout' data-docs-url=${docsUrl} data-image-url=${imageUrl}></div>
|
||||
`);
|
||||
});
|
||||
|
||||
|
@ -30,13 +25,13 @@ describe('Pipeline Schedule Callout', () => {
|
|||
expect(calloutComponent).toBeDefined();
|
||||
});
|
||||
|
||||
it('correctly sets illustrationSvg', () => {
|
||||
expect(calloutComponent.illustrationSvg).toContain('<svg');
|
||||
});
|
||||
|
||||
it('correctly sets docsUrl', () => {
|
||||
expect(calloutComponent.docsUrl).toContain(docsUrl);
|
||||
});
|
||||
|
||||
it('correctly sets imageUrl', () => {
|
||||
expect(calloutComponent.imageUrl).toContain(imageUrl);
|
||||
});
|
||||
});
|
||||
|
||||
describe(`when ${cookieKey} cookie is set`, () => {
|
||||
|
@ -68,8 +63,8 @@ describe('Pipeline Schedule Callout', () => {
|
|||
expect(calloutComponent.$el.querySelector('.bordered-box')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('renders the callout svg', () => {
|
||||
expect(calloutComponent.$el.outerHTML).toContain('<svg');
|
||||
it('renders the callout img', () => {
|
||||
expect(calloutComponent.$el.outerHTML).toContain('<img');
|
||||
});
|
||||
|
||||
it('renders the callout title', () => {
|
||||
|
|
|
@ -242,6 +242,7 @@ ci_pipelines:
|
|||
- latest_builds_report_results
|
||||
- messages
|
||||
- pipeline_artifacts
|
||||
- latest_statuses
|
||||
ci_refs:
|
||||
- project
|
||||
- ci_pipelines
|
||||
|
|
|
@ -1508,12 +1508,44 @@ RSpec.describe Notify do
|
|||
)
|
||||
end
|
||||
|
||||
describe 'group invitation' do
|
||||
describe 'invitations' do
|
||||
let(:owner) { create(:user).tap { |u| group.add_user(u, Gitlab::Access::OWNER) } }
|
||||
let(:group_member) { invite_to_group(group, inviter: inviter) }
|
||||
let(:inviter) { owner }
|
||||
|
||||
subject { described_class.member_invited_email('group', group_member.id, group_member.invite_token) }
|
||||
subject { described_class.member_invited_email('Group', group_member.id, group_member.invite_token) }
|
||||
|
||||
shared_examples "tracks the 'sent' event for the invitation reminders experiment" do
|
||||
before do
|
||||
stub_experiment(invitation_reminders: true)
|
||||
allow(Gitlab::Experimentation).to receive(:enabled_for_attribute?).with(:invitation_reminders, group_member.invite_email).and_return(experimental_group)
|
||||
end
|
||||
|
||||
it "tracks the 'sent' event", :snowplow do
|
||||
subject.deliver_now
|
||||
|
||||
expect_snowplow_event(
|
||||
category: 'Growth::Acquisition::Experiment::InvitationReminders',
|
||||
label: Digest::MD5.hexdigest(group_member.to_global_id.to_s),
|
||||
property: experimental_group ? 'experimental_group' : 'control_group',
|
||||
action: 'sent'
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'tracking for the invitation reminders experiment' do
|
||||
context 'when invite email is in the experimental group' do
|
||||
let(:experimental_group) { true }
|
||||
|
||||
it_behaves_like "tracks the 'sent' event for the invitation reminders experiment"
|
||||
end
|
||||
|
||||
context 'when invite email is in the control group' do
|
||||
let(:experimental_group) { false }
|
||||
|
||||
it_behaves_like "tracks the 'sent' event for the invitation reminders experiment"
|
||||
end
|
||||
end
|
||||
|
||||
context 'when invite_email_experiment is disabled' do
|
||||
before do
|
||||
|
|
|
@ -2436,7 +2436,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
|
|||
end
|
||||
|
||||
describe '#retry_failed' do
|
||||
let(:latest_status) { pipeline.statuses.latest.pluck(:status) }
|
||||
let(:latest_status) { pipeline.latest_statuses.pluck(:status) }
|
||||
|
||||
before do
|
||||
stub_not_protect_default_branch
|
||||
|
|
|
@ -205,6 +205,16 @@ RSpec.describe Member do
|
|||
it { expect(described_class.non_request).to include @accepted_request_member }
|
||||
end
|
||||
|
||||
describe '.not_accepted_invitations' do
|
||||
let_it_be(:not_accepted_invitation) { create(:project_member, :invited) }
|
||||
let_it_be(:accepted_invitation) { create(:project_member, :invited, invite_accepted_at: Date.today) }
|
||||
|
||||
subject { described_class.not_accepted_invitations }
|
||||
|
||||
it { is_expected.to include(not_accepted_invitation) }
|
||||
it { is_expected.not_to include(accepted_invitation) }
|
||||
end
|
||||
|
||||
describe '.not_accepted_invitations_by_user' do
|
||||
let(:invited_by_user) { create(:project_member, :invited, project: project, created_by: @owner_user) }
|
||||
|
||||
|
@ -218,6 +228,29 @@ RSpec.describe Member do
|
|||
it { is_expected.to contain_exactly(invited_by_user) }
|
||||
end
|
||||
|
||||
describe '.not_expired' do
|
||||
let_it_be(:expiring_yesterday) { create(:group_member, expires_at: 1.day.ago) }
|
||||
let_it_be(:expiring_today) { create(:group_member, expires_at: Date.today) }
|
||||
let_it_be(:expiring_tomorrow) { create(:group_member, expires_at: 1.day.from_now) }
|
||||
let_it_be(:not_expiring) { create(:group_member) }
|
||||
|
||||
subject { described_class.not_expired }
|
||||
|
||||
it { is_expected.not_to include(expiring_yesterday, expiring_today) }
|
||||
it { is_expected.to include(expiring_tomorrow, not_expiring) }
|
||||
end
|
||||
|
||||
describe '.last_ten_days_excluding_today' do
|
||||
let_it_be(:created_today) { create(:group_member, created_at: Date.today.beginning_of_day) }
|
||||
let_it_be(:created_yesterday) { create(:group_member, created_at: 1.day.ago) }
|
||||
let_it_be(:created_eleven_days_ago) { create(:group_member, created_at: 11.days.ago) }
|
||||
|
||||
subject { described_class.last_ten_days_excluding_today }
|
||||
|
||||
it { is_expected.to include(created_yesterday) }
|
||||
it { is_expected.not_to include(created_today, created_eleven_days_ago) }
|
||||
end
|
||||
|
||||
describe '.search_invite_email' do
|
||||
it 'returns only members the matching e-mail' do
|
||||
create(:group_member, :invited)
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
# frozen_string_literal: true
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe API::DebianGroupPackages do
|
||||
include HttpBasicAuthHelpers
|
||||
include WorkhorseHelpers
|
||||
|
||||
include_context 'Debian repository shared context', :group do
|
||||
describe 'GET groups/:id/-/packages/debian/dists/*distribution/Release.gpg' do
|
||||
let(:url) { "/groups/#{group.id}/-/packages/debian/dists/#{distribution}/Release.gpg" }
|
||||
|
||||
it_behaves_like 'Debian group repository GET endpoint', :not_found, nil
|
||||
end
|
||||
|
||||
describe 'GET groups/:id/-/packages/debian/dists/*distribution/Release' do
|
||||
let(:url) { "/groups/#{group.id}/-/packages/debian/dists/#{distribution}/Release" }
|
||||
|
||||
it_behaves_like 'Debian group repository GET endpoint', :success, 'TODO Release'
|
||||
end
|
||||
|
||||
describe 'GET groups/:id/-/packages/debian/dists/*distribution/InRelease' do
|
||||
let(:url) { "/groups/#{group.id}/-/packages/debian/dists/#{distribution}/InRelease" }
|
||||
|
||||
it_behaves_like 'Debian group repository GET endpoint', :not_found, nil
|
||||
end
|
||||
|
||||
describe 'GET groups/:id/-/packages/debian/dists/*distribution/:component/binary-:architecture/Packages' do
|
||||
let(:url) { "/groups/#{group.id}/-/packages/debian/dists/#{distribution}/#{component}/binary-#{architecture}/Packages" }
|
||||
|
||||
it_behaves_like 'Debian group repository GET endpoint', :success, 'TODO Packages'
|
||||
end
|
||||
|
||||
describe 'GET groups/:id/-/packages/debian/pool/:component/:letter/:source_package/:file_name' do
|
||||
let(:url) { "/groups/#{group.id}/-/packages/debian/pool/#{component}/#{letter}/#{source_package}/#{package_name}_#{package_version}_#{architecture}.deb" }
|
||||
|
||||
it_behaves_like 'Debian group repository GET endpoint', :success, 'TODO File'
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,46 @@
|
|||
# frozen_string_literal: true
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe API::DebianProjectPackages do
|
||||
include HttpBasicAuthHelpers
|
||||
include WorkhorseHelpers
|
||||
|
||||
include_context 'Debian repository shared context', :project do
|
||||
describe 'GET projects/:id/-/packages/debian/dists/*distribution/Release.gpg' do
|
||||
let(:url) { "/projects/#{project.id}/-/packages/debian/dists/#{distribution}/Release.gpg" }
|
||||
|
||||
it_behaves_like 'Debian project repository GET endpoint', :not_found, nil
|
||||
end
|
||||
|
||||
describe 'GET projects/:id/-/packages/debian/dists/*distribution/Release' do
|
||||
let(:url) { "/projects/#{project.id}/-/packages/debian/dists/#{distribution}/Release" }
|
||||
|
||||
it_behaves_like 'Debian project repository GET endpoint', :success, 'TODO Release'
|
||||
end
|
||||
|
||||
describe 'GET projects/:id/-/packages/debian/dists/*distribution/InRelease' do
|
||||
let(:url) { "/projects/#{project.id}/-/packages/debian/dists/#{distribution}/InRelease" }
|
||||
|
||||
it_behaves_like 'Debian project repository GET endpoint', :not_found, nil
|
||||
end
|
||||
|
||||
describe 'GET projects/:id/-/packages/debian/dists/*distribution/:component/binary-:architecture/Packages' do
|
||||
let(:url) { "/projects/#{project.id}/-/packages/debian/dists/#{distribution}/#{component}/binary-#{architecture}/Packages" }
|
||||
|
||||
it_behaves_like 'Debian project repository GET endpoint', :success, 'TODO Packages'
|
||||
end
|
||||
|
||||
describe 'GET projects/:id/-/packages/debian/pool/:component/:letter/:source_package/:file_name' do
|
||||
let(:url) { "/projects/#{project.id}/-/packages/debian/pool/#{component}/#{letter}/#{source_package}/#{package_name}_#{package_version}_#{architecture}.deb" }
|
||||
|
||||
it_behaves_like 'Debian project repository GET endpoint', :success, 'TODO File'
|
||||
end
|
||||
|
||||
describe 'PUT projects/:id/-/packages/debian/incoming/:file_name' do
|
||||
let(:method) { :put }
|
||||
let(:url) { "/projects/#{project.id}/-/packages/debian/incoming/#{file_name}" }
|
||||
|
||||
it_behaves_like 'Debian project repository PUT endpoint', :created, nil
|
||||
end
|
||||
end
|
||||
end
|
|
@ -3,12 +3,11 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe MergeRequestPollCachedWidgetEntity do
|
||||
include ProjectForksHelper
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
let(:project) { create :project, :repository }
|
||||
let(:resource) { create(:merge_request, source_project: project, target_project: project) }
|
||||
let(:user) { create(:user) }
|
||||
let_it_be(:project, refind: true) { create :project, :repository }
|
||||
let_it_be(:resource, refind: true) { create(:merge_request, source_project: project, target_project: project) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
let(:request) { double('request', current_user: user, project: project) }
|
||||
|
||||
|
@ -174,8 +173,6 @@ RSpec.describe MergeRequestPollCachedWidgetEntity do
|
|||
end
|
||||
|
||||
context 'when auto merge is not enabled' do
|
||||
let(:resource) { create(:merge_request) }
|
||||
|
||||
it 'returns auto merge related information' do
|
||||
expect(subject[:auto_merge_enabled]).to be_falsy
|
||||
end
|
||||
|
@ -215,16 +212,5 @@ RSpec.describe MergeRequestPollCachedWidgetEntity do
|
|||
expect(subject[:commits_without_merge_commits].size).to eq(12)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when merge request is not mergeable' do
|
||||
before do
|
||||
allow(resource).to receive(:mergeable?).and_return(false)
|
||||
end
|
||||
|
||||
it 'does not have default_squash_commit_message and commits_without_merge_commits' do
|
||||
expect(subject[:default_squash_commit_message]).to eq(nil)
|
||||
expect(subject[:commits_without_merge_commits]).to eq(nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -155,7 +155,7 @@ RSpec.describe PipelineSerializer do
|
|||
|
||||
it 'verifies number of queries', :request_store do
|
||||
recorded = ActiveRecord::QueryRecorder.new { subject }
|
||||
expected_queries = Gitlab.ee? ? 43 : 40
|
||||
expected_queries = Gitlab.ee? ? 39 : 36
|
||||
|
||||
expect(recorded.count).to be_within(2).of(expected_queries)
|
||||
expect(recorded.cached_count).to eq(0)
|
||||
|
@ -176,7 +176,7 @@ RSpec.describe PipelineSerializer do
|
|||
# pipeline. With the same ref this check is cached but if refs are
|
||||
# different then there is an extra query per ref
|
||||
# https://gitlab.com/gitlab-org/gitlab-foss/issues/46368
|
||||
expected_queries = Gitlab.ee? ? 49 : 46
|
||||
expected_queries = Gitlab.ee? ? 42 : 39
|
||||
|
||||
expect(recorded.count).to be_within(2).of(expected_queries)
|
||||
expect(recorded.cached_count).to eq(0)
|
||||
|
@ -199,11 +199,10 @@ RSpec.describe PipelineSerializer do
|
|||
it 'verifies number of queries', :request_store do
|
||||
recorded = ActiveRecord::QueryRecorder.new { subject }
|
||||
|
||||
# 99 queries by default + 2 related to preloading
|
||||
# :source_pipeline and :source_job
|
||||
# Existing numbers are high and require performance optimization
|
||||
# Ongoing issue:
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/225156
|
||||
expected_queries = Gitlab.ee? ? 95 : 86
|
||||
expected_queries = Gitlab.ee? ? 85 : 76
|
||||
|
||||
expect(recorded.count).to be_within(2).of(expected_queries)
|
||||
expect(recorded.cached_count).to eq(0)
|
||||
|
|
|
@ -43,12 +43,12 @@ RSpec.shared_context 'Pipeline Processing Service Tests With Yaml' do
|
|||
{
|
||||
pipeline: pipeline.status,
|
||||
stages: pipeline.stages.pluck(:name, :status).to_h,
|
||||
jobs: pipeline.statuses.latest.pluck(:name, :status).to_h
|
||||
jobs: pipeline.latest_statuses.pluck(:name, :status).to_h
|
||||
}
|
||||
end
|
||||
|
||||
def event_on_jobs(event, job_names)
|
||||
statuses = pipeline.statuses.latest.by_name(job_names).to_a
|
||||
statuses = pipeline.latest_statuses.by_name(job_names).to_a
|
||||
expect(statuses.count).to eq(job_names.count) # ensure that we have the same counts
|
||||
|
||||
statuses.each { |status| status.public_send("#{event}!") }
|
||||
|
|
|
@ -0,0 +1,309 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.shared_context 'Debian repository shared context' do |object_type|
|
||||
before do
|
||||
stub_feature_flags(debian_packages: true)
|
||||
end
|
||||
|
||||
if object_type == :project
|
||||
let(:project) { create(:project, :public) }
|
||||
elsif object_type == :group
|
||||
let(:group) { create(:group, :public) }
|
||||
end
|
||||
|
||||
let(:user) { create(:user) }
|
||||
let(:personal_access_token) { create(:personal_access_token, user: user) }
|
||||
|
||||
let(:distribution) { 'bullseye' }
|
||||
let(:component) { 'main' }
|
||||
let(:architecture) { 'amd64' }
|
||||
let(:source_package) { 'sample' }
|
||||
let(:letter) { source_package[0..2] == 'lib' ? source_package[0..3] : source_package[0] }
|
||||
let(:package_name) { 'libsample0' }
|
||||
let(:package_version) { '1.2.3~alpha2-1' }
|
||||
let(:file_name) { "#{package_name}_#{package_version}_#{architecture}.deb" }
|
||||
|
||||
let(:method) { :get }
|
||||
|
||||
let(:workhorse_params) do
|
||||
if method == :put
|
||||
file_upload = fixture_file_upload("spec/fixtures/packages/debian/#{file_name}")
|
||||
{ file: file_upload }
|
||||
else
|
||||
{}
|
||||
end
|
||||
end
|
||||
|
||||
let(:params) { workhorse_params }
|
||||
|
||||
let(:auth_headers) { {} }
|
||||
let(:workhorse_headers) do
|
||||
if method == :put
|
||||
workhorse_token = JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256')
|
||||
{ 'GitLab-Workhorse' => '1.0', Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => workhorse_token }
|
||||
else
|
||||
{}
|
||||
end
|
||||
end
|
||||
|
||||
let(:headers) { auth_headers.merge(workhorse_headers) }
|
||||
|
||||
let(:send_rewritten_field) { true }
|
||||
|
||||
subject do
|
||||
if method == :put
|
||||
workhorse_finalize(
|
||||
api(url),
|
||||
method: method,
|
||||
file_key: :file,
|
||||
params: params,
|
||||
headers: headers,
|
||||
send_rewritten_field: send_rewritten_field
|
||||
)
|
||||
else
|
||||
send method, api(url), headers: headers, params: params
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.shared_context 'Debian repository auth headers' do |user_role, user_token, auth_method = :token|
|
||||
let(:token) { user_token ? personal_access_token.token : 'wrong' }
|
||||
|
||||
let(:auth_headers) do
|
||||
if user_role == :anonymous
|
||||
{}
|
||||
elsif auth_method == :token
|
||||
{ 'Private-Token' => token }
|
||||
else
|
||||
basic_auth_header(user.username, token)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.shared_context 'Debian repository project access' do |project_visibility_level, user_role, user_token, auth_method|
|
||||
include_context 'Debian repository auth headers', user_role, user_token, auth_method do
|
||||
before do
|
||||
project.update_column(:visibility_level, Gitlab::VisibilityLevel.const_get(project_visibility_level, false))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'Debian project repository GET request' do |user_role, add_member, status, body|
|
||||
context "for user type #{user_role}" do
|
||||
before do
|
||||
project.send("add_#{user_role}", user) if add_member && user_role != :anonymous
|
||||
end
|
||||
|
||||
and_body = body.nil? ? '' : ' and expected body'
|
||||
|
||||
it "returns #{status}#{and_body}" do
|
||||
subject
|
||||
|
||||
expect(response).to have_gitlab_http_status(status)
|
||||
|
||||
unless body.nil?
|
||||
expect(response.body).to eq(body)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'Debian project repository PUT request' do |user_role, add_member, status, body|
|
||||
context "for user type #{user_role}" do
|
||||
before do
|
||||
project.send("add_#{user_role}", user) if add_member && user_role != :anonymous
|
||||
end
|
||||
|
||||
and_body = body.nil? ? '' : ' and expected body'
|
||||
|
||||
if status == :created
|
||||
it 'creates package files' do
|
||||
pending "Debian package creation not implemented"
|
||||
expect { subject }
|
||||
.to change { project.packages.debian.count }.by(1)
|
||||
|
||||
expect(response).to have_gitlab_http_status(status)
|
||||
|
||||
unless body.nil?
|
||||
expect(response.body).to eq(body)
|
||||
end
|
||||
end
|
||||
it_behaves_like 'a gitlab tracking event', described_class.name, 'push_package'
|
||||
else
|
||||
it "returns #{status}#{and_body}" do
|
||||
subject
|
||||
|
||||
expect(response).to have_gitlab_http_status(status)
|
||||
|
||||
unless body.nil?
|
||||
expect(response.body).to eq(body)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'rejects Debian access with unknown project id' do
|
||||
context 'with an unknown project' do
|
||||
let(:project) { double(id: non_existing_record_id) }
|
||||
|
||||
context 'as anonymous' do
|
||||
it_behaves_like 'Debian project repository GET request', :anonymous, true, :not_found, nil
|
||||
end
|
||||
|
||||
context 'as authenticated user' do
|
||||
subject { get api(url), headers: basic_auth_header(user.username, personal_access_token.token) }
|
||||
|
||||
it_behaves_like 'Debian project repository GET request', :anonymous, true, :not_found, nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'Debian project repository GET endpoint' do |success_status, success_body|
|
||||
context 'with valid project' do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
where(:project_visibility_level, :user_role, :member, :user_token, :expected_status, :expected_body) do
|
||||
'PUBLIC' | :developer | true | true | success_status | success_body
|
||||
'PUBLIC' | :guest | true | true | success_status | success_body
|
||||
'PUBLIC' | :developer | true | false | success_status | success_body
|
||||
'PUBLIC' | :guest | true | false | success_status | success_body
|
||||
'PUBLIC' | :developer | false | true | success_status | success_body
|
||||
'PUBLIC' | :guest | false | true | success_status | success_body
|
||||
'PUBLIC' | :developer | false | false | success_status | success_body
|
||||
'PUBLIC' | :guest | false | false | success_status | success_body
|
||||
'PUBLIC' | :anonymous | false | true | success_status | success_body
|
||||
'PRIVATE' | :developer | true | true | success_status | success_body
|
||||
'PRIVATE' | :guest | true | true | :forbidden | nil
|
||||
'PRIVATE' | :developer | true | false | :not_found | nil
|
||||
'PRIVATE' | :guest | true | false | :not_found | nil
|
||||
'PRIVATE' | :developer | false | true | :not_found | nil
|
||||
'PRIVATE' | :guest | false | true | :not_found | nil
|
||||
'PRIVATE' | :developer | false | false | :not_found | nil
|
||||
'PRIVATE' | :guest | false | false | :not_found | nil
|
||||
'PRIVATE' | :anonymous | false | true | :not_found | nil
|
||||
end
|
||||
|
||||
with_them do
|
||||
include_context 'Debian repository project access', params[:project_visibility_level], params[:user_role], params[:user_token], :basic do
|
||||
it_behaves_like 'Debian project repository GET request', params[:user_role], params[:member], params[:expected_status], params[:expected_body]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'rejects Debian access with unknown project id'
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'Debian project repository PUT endpoint' do |success_status, success_body|
|
||||
context 'with valid project' do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
where(:project_visibility_level, :user_role, :member, :user_token, :expected_status, :expected_body) do
|
||||
'PUBLIC' | :developer | true | true | success_status | nil
|
||||
'PUBLIC' | :guest | true | true | :forbidden | nil
|
||||
'PUBLIC' | :developer | true | false | :unauthorized | nil
|
||||
'PUBLIC' | :guest | true | false | :unauthorized | nil
|
||||
'PUBLIC' | :developer | false | true | :forbidden | nil
|
||||
'PUBLIC' | :guest | false | true | :forbidden | nil
|
||||
'PUBLIC' | :developer | false | false | :unauthorized | nil
|
||||
'PUBLIC' | :guest | false | false | :unauthorized | nil
|
||||
'PUBLIC' | :anonymous | false | true | :unauthorized | nil
|
||||
'PRIVATE' | :developer | true | true | success_status | nil
|
||||
'PRIVATE' | :guest | true | true | :forbidden | nil
|
||||
'PRIVATE' | :developer | true | false | :not_found | nil
|
||||
'PRIVATE' | :guest | true | false | :not_found | nil
|
||||
'PRIVATE' | :developer | false | true | :not_found | nil
|
||||
'PRIVATE' | :guest | false | true | :not_found | nil
|
||||
'PRIVATE' | :developer | false | false | :not_found | nil
|
||||
'PRIVATE' | :guest | false | false | :not_found | nil
|
||||
'PRIVATE' | :anonymous | false | true | :not_found | nil
|
||||
end
|
||||
|
||||
with_them do
|
||||
include_context 'Debian repository project access', params[:project_visibility_level], params[:user_role], params[:user_token], :basic do
|
||||
it_behaves_like 'Debian project repository PUT request', params[:user_role], params[:member], params[:expected_status], params[:expected_body]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'rejects Debian access with unknown project id'
|
||||
end
|
||||
|
||||
RSpec.shared_context 'Debian repository group access' do |group_visibility_level, user_role, user_token, auth_method|
|
||||
include_context 'Debian repository auth headers', user_role, user_token, auth_method do
|
||||
before do
|
||||
group.update_column(:visibility_level, Gitlab::VisibilityLevel.const_get(group_visibility_level, false))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'Debian group repository GET request' do |user_role, add_member, status, body|
|
||||
context "for user type #{user_role}" do
|
||||
before do
|
||||
group.send("add_#{user_role}", user) if add_member && user_role != :anonymous
|
||||
end
|
||||
|
||||
and_body = body.nil? ? '' : ' and expected body'
|
||||
|
||||
it "returns #{status}#{and_body}" do
|
||||
subject
|
||||
|
||||
expect(response).to have_gitlab_http_status(status)
|
||||
|
||||
unless body.nil?
|
||||
expect(response.body).to eq(body)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'rejects Debian access with unknown group id' do
|
||||
context 'with an unknown group' do
|
||||
let(:group) { double(id: non_existing_record_id) }
|
||||
|
||||
context 'as anonymous' do
|
||||
it_behaves_like 'Debian group repository GET request', :anonymous, true, :not_found, nil
|
||||
end
|
||||
|
||||
context 'as authenticated user' do
|
||||
subject { get api(url), headers: basic_auth_header(user.username, personal_access_token.token) }
|
||||
|
||||
it_behaves_like 'Debian group repository GET request', :anonymous, true, :not_found, nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'Debian group repository GET endpoint' do |success_status, success_body|
|
||||
context 'with valid group' do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
where(:group_visibility_level, :user_role, :member, :user_token, :expected_status, :expected_body) do
|
||||
'PUBLIC' | :developer | true | true | success_status | success_body
|
||||
'PUBLIC' | :guest | true | true | success_status | success_body
|
||||
'PUBLIC' | :developer | true | false | success_status | success_body
|
||||
'PUBLIC' | :guest | true | false | success_status | success_body
|
||||
'PUBLIC' | :developer | false | true | success_status | success_body
|
||||
'PUBLIC' | :guest | false | true | success_status | success_body
|
||||
'PUBLIC' | :developer | false | false | success_status | success_body
|
||||
'PUBLIC' | :guest | false | false | success_status | success_body
|
||||
'PUBLIC' | :anonymous | false | true | success_status | success_body
|
||||
'PRIVATE' | :developer | true | true | success_status | success_body
|
||||
'PRIVATE' | :guest | true | true | :forbidden | nil
|
||||
'PRIVATE' | :developer | true | false | :not_found | nil
|
||||
'PRIVATE' | :guest | true | false | :not_found | nil
|
||||
'PRIVATE' | :developer | false | true | :not_found | nil
|
||||
'PRIVATE' | :guest | false | true | :not_found | nil
|
||||
'PRIVATE' | :developer | false | false | :not_found | nil
|
||||
'PRIVATE' | :guest | false | false | :not_found | nil
|
||||
'PRIVATE' | :anonymous | false | true | :not_found | nil
|
||||
end
|
||||
|
||||
with_them do
|
||||
include_context 'Debian repository group access', params[:group_visibility_level], params[:user_role], params[:user_token], :basic do
|
||||
it_behaves_like 'Debian group repository GET request', params[:user_role], params[:member], params[:expected_status], params[:expected_body]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'rejects Debian access with unknown group id'
|
||||
end
|
Loading…
Reference in New Issue