Commit 0dfbcd8f authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent cd631619
...@@ -42,19 +42,12 @@ export default { ...@@ -42,19 +42,12 @@ export default {
return { return {
showDetail: false, showDetail: false,
detailIssue: boardsStore.detail, detailIssue: boardsStore.detail,
multiSelect: boardsStore.multiSelect,
}; };
}, },
computed: { computed: {
issueDetailVisible() { issueDetailVisible() {
return this.detailIssue.issue && this.detailIssue.issue.id === this.issue.id; return this.detailIssue.issue && this.detailIssue.issue.id === this.issue.id;
}, },
multiSelectVisible() {
return this.multiSelect.list.findIndex(issue => issue.id === this.issue.id) > -1;
},
canMultiSelect() {
return gon.features && gon.features.multiSelectBoard;
},
}, },
methods: { methods: {
mouseDown() { mouseDown() {
...@@ -65,20 +58,14 @@ export default { ...@@ -65,20 +58,14 @@ export default {
}, },
showIssue(e) { showIssue(e) {
if (e.target.classList.contains('js-no-trigger')) return; if (e.target.classList.contains('js-no-trigger')) return;
if (this.showDetail) { if (this.showDetail) {
this.showDetail = false; this.showDetail = false;
// If CMD or CTRL is clicked
const isMultiSelect = this.canMultiSelect && (e.ctrlKey || e.metaKey);
if (boardsStore.detail.issue && boardsStore.detail.issue.id === this.issue.id) { if (boardsStore.detail.issue && boardsStore.detail.issue.id === this.issue.id) {
eventHub.$emit('clearDetailIssue', isMultiSelect); eventHub.$emit('clearDetailIssue');
if (isMultiSelect) {
eventHub.$emit('newDetailIssue', this.issue, isMultiSelect);
}
} else { } else {
eventHub.$emit('newDetailIssue', this.issue, isMultiSelect); eventHub.$emit('newDetailIssue', this.issue);
boardsStore.setListDetail(this.list); boardsStore.setListDetail(this.list);
} }
} }
...@@ -90,7 +77,6 @@ export default { ...@@ -90,7 +77,6 @@ export default {
<template> <template>
<li <li
:class="{ :class="{
'multi-select': multiSelectVisible,
'user-can-drag': !disabled && issue.id, 'user-can-drag': !disabled && issue.id,
'is-disabled': disabled || !issue.id, 'is-disabled': disabled || !issue.id,
'is-active': issueDetailVisible, 'is-active': issueDetailVisible,
......
<script> <script>
import { Sortable, MultiDrag } from 'sortablejs'; /* eslint-disable @gitlab/vue-i18n/no-bare-strings */
import Sortable from 'sortablejs';
import { GlLoadingIcon } from '@gitlab/ui'; import { GlLoadingIcon } from '@gitlab/ui';
import _ from 'underscore';
import boardNewIssue from './board_new_issue.vue'; import boardNewIssue from './board_new_issue.vue';
import boardCard from './board_card.vue'; import boardCard from './board_card.vue';
import eventHub from '../eventhub'; import eventHub from '../eventhub';
import boardsStore from '../stores/boards_store'; import boardsStore from '../stores/boards_store';
import { sprintf, __ } from '~/locale'; import { getBoardSortableDefaultOptions, sortableStart } from '../mixins/sortable_default_options';
import createFlash from '~/flash';
import {
getBoardSortableDefaultOptions,
sortableStart,
sortableEnd,
} from '../mixins/sortable_default_options';
if (gon.features && gon.features.multiSelectBoard) {
Sortable.mount(new MultiDrag());
}
export default { export default {
name: 'BoardList', name: 'BoardList',
...@@ -64,14 +54,6 @@ export default { ...@@ -64,14 +54,6 @@ export default {
showIssueForm: false, showIssueForm: false,
}; };
}, },
computed: {
paginatedIssueText() {
return sprintf(__('Showing %{pageSize} of %{total} issues'), {
pageSize: this.list.issues.length,
total: this.list.issuesSize,
});
},
},
watch: { watch: {
filters: { filters: {
handler() { handler() {
...@@ -105,20 +87,11 @@ export default { ...@@ -105,20 +87,11 @@ export default {
eventHub.$on(`scroll-board-list-${this.list.id}`, this.scrollToTop); eventHub.$on(`scroll-board-list-${this.list.id}`, this.scrollToTop);
}, },
mounted() { mounted() {
const multiSelectOpts = {};
if (gon.features && gon.features.multiSelectBoard) {
multiSelectOpts.multiDrag = true;
multiSelectOpts.selectedClass = 'js-multi-select';
multiSelectOpts.animation = 500;
}
const options = getBoardSortableDefaultOptions({ const options = getBoardSortableDefaultOptions({
scroll: true, scroll: true,
disabled: this.disabled, disabled: this.disabled,
filter: '.board-list-count, .is-disabled', filter: '.board-list-count, .is-disabled',
dataIdAttr: 'data-issue-id', dataIdAttr: 'data-issue-id',
removeCloneOnHide: false,
...multiSelectOpts,
group: { group: {
name: 'issues', name: 'issues',
/** /**
...@@ -172,66 +145,25 @@ export default { ...@@ -172,66 +145,25 @@ export default {
card.showDetail = false; card.showDetail = false;
const { list } = card; const { list } = card;
const issue = list.findIssue(Number(e.item.dataset.issueId)); const issue = list.findIssue(Number(e.item.dataset.issueId));
boardsStore.startMoving(list, issue); boardsStore.startMoving(list, issue);
sortableStart(); sortableStart();
}, },
onAdd: e => { onAdd: e => {
const { items = [], newIndicies = [] } = e; boardsStore.moveIssueToList(
if (items.length) { boardsStore.moving.list,
// Not using e.newIndex here instead taking a min of all this.list,
// the newIndicies. Basically we have to find that during boardsStore.moving.issue,
// a drop what is the index we're going to start putting e.newIndex,
// all the dropped elements from. );
const newIndex = Math.min(...newIndicies.map(obj => obj.index).filter(i => i !== -1));
const issues = items.map(item =>
boardsStore.moving.list.findIssue(Number(item.dataset.issueId)),
);
boardsStore.moveMultipleIssuesToList({ this.$nextTick(() => {
listFrom: boardsStore.moving.list, e.item.remove();
listTo: this.list, });
issues,
newIndex,
});
} else {
boardsStore.moveIssueToList(
boardsStore.moving.list,
this.list,
boardsStore.moving.issue,
e.newIndex,
);
this.$nextTick(() => {
e.item.remove();
});
}
}, },
onUpdate: e => { onUpdate: e => {
const sortedArray = this.sortable.toArray().filter(id => id !== '-1'); const sortedArray = this.sortable.toArray().filter(id => id !== '-1');
const { items = [], newIndicies = [], oldIndicies = [] } = e;
if (items.length) {
const newIndex = Math.min(...newIndicies.map(obj => obj.index));
const issues = items.map(item =>
boardsStore.moving.list.findIssue(Number(item.dataset.issueId)),
);
boardsStore.moveMultipleIssuesInList({
list: this.list,
issues,
oldIndicies: oldIndicies.map(obj => obj.index),
newIndex,
idArray: sortedArray,
});
e.items.forEach(el => {
Sortable.utils.deselect(el);
});
boardsStore.clearMultiSelect();
return;
}
boardsStore.moveIssueInList( boardsStore.moveIssueInList(
this.list, this.list,
boardsStore.moving.issue, boardsStore.moving.issue,
...@@ -240,133 +172,9 @@ export default { ...@@ -240,133 +172,9 @@ export default {
sortedArray, sortedArray,
); );
}, },
onEnd: e => {
const { items = [], clones = [], to } = e;
// This is not a multi select operation
if (!items.length && !clones.length) {
sortableEnd();
return;
}
let toList;
if (to) {
const containerEl = to.closest('.js-board-list');
toList = boardsStore.findList('id', Number(containerEl.dataset.board));
}
/**
* onEnd is called irrespective if the cards were moved in the
* same list or the other list. Don't remove items if it's same list.
*/
const isSameList = toList && toList.id === this.list.id;
if (toList && !isSameList && boardsStore.shouldRemoveIssue(this.list, toList)) {
const issues = items.map(item => this.list.findIssue(Number(item.dataset.issueId)));
if (_.compact(issues).length && !boardsStore.issuesAreContiguous(this.list, issues)) {
const indexes = [];
const ids = this.list.issues.map(i => i.id);
issues.forEach(issue => {
const index = ids.indexOf(issue.id);
if (index > -1) {
indexes.push(index);
}
});
// Descending sort because splice would cause index discrepancy otherwise
const sortedIndexes = indexes.sort((a, b) => (a < b ? 1 : -1));
sortedIndexes.forEach(i => {
/**
* **setTimeout and splice each element one-by-one in a loop
* is intended.**
*
* The problem here is all the indexes are in the list but are
* non-contiguous. Due to that, when we splice all the indexes,
* at once, Vue -- during a re-render -- is unable to find reference
* nodes and the entire app crashes.
*
* If the indexes are contiguous, this piece of code is not
* executed. If it is, this is a possible regression. Only when
* issue indexes are far apart, this logic should ever kick in.
*/
setTimeout(() => {
this.list.issues.splice(i, 1);
}, 0);
});
}
}
if (!toList) {
createFlash(__('Something went wrong while performing the action.'));
}
if (!isSameList) {
boardsStore.clearMultiSelect();
// Since Vue's list does not re-render the same keyed item, we'll
// remove `multi-select` class to express it's unselected
if (clones && clones.length) {
clones.forEach(el => el.classList.remove('multi-select'));
}
// Due to some bug which I am unable to figure out
// Sortable does not deselect some pending items from the
// source list.
// We'll just do it forcefully here.
Array.from(document.querySelectorAll('.js-multi-select') || []).forEach(item => {
Sortable.utils.deselect(item);
});
/**
* SortableJS leaves all the moving items "as is" on the DOM.
* Vue picks up and rehydrates the DOM, but we need to explicity
* remove the "trash" items from the DOM.
*
* This is in parity to the logic on single item move from a list/in
* a list. For reference, look at the implementation of onAdd method.
*/
this.$nextTick(() => {
if (items && items.length) {
items.forEach(item => {
item.remove();
});
}
});
}
sortableEnd();
},
onMove(e) { onMove(e) {
return !e.related.classList.contains('board-list-count'); return !e.related.classList.contains('board-list-count');
}, },
onSelect(e) {
const {
item: { classList },
} = e;
if (
classList &&
classList.contains('js-multi-select') &&
!classList.contains('multi-select')
) {
Sortable.utils.deselect(e.item);
}
},
onDeselect: e => {
const {
item: { dataset, classList },
} = e;
if (
classList &&
classList.contains('multi-select') &&
!classList.contains('js-multi-select')
) {
const issue = this.list.findIssue(Number(dataset.issueId));
boardsStore.toggleMultiSelect(issue);
}
},
}); });
this.sortable = Sortable.create(this.$refs.list, options); this.sortable = Sortable.create(this.$refs.list, options);
...@@ -452,7 +260,7 @@ export default { ...@@ -452,7 +260,7 @@ export default {
<li v-if="showCount" class="board-list-count text-center" data-issue-id="-1"> <li v-if="showCount" class="board-list-count text-center" data-issue-id="-1">
<gl-loading-icon v-show="list.loadingMore" label="Loading more issues" /> <gl-loading-icon v-show="list.loadingMore" label="Loading more issues" />
<span v-if="list.issues.length === list.issuesSize">{{ __('Showing all issues') }}</span> <span v-if="list.issues.length === list.issuesSize">{{ __('Showing all issues') }}</span>
<span v-else>{{ paginatedIssueText }}</span> <span v-else> Showing {{ list.issues.length }} of {{ list.issuesSize }} issues </span>
</li> </li>
</ul> </ul>
</div> </div>
......
export const ListType = {
assignee: 'assignee',
milestone: 'milestone',
backlog: 'backlog',
closed: 'closed',
label: 'label',
};
export default {
ListType,
};
...@@ -146,7 +146,7 @@ export default () => { ...@@ -146,7 +146,7 @@ export default () => {
updateTokens() { updateTokens() {
this.filterManager.updateTokens(); this.filterManager.updateTokens();
}, },
updateDetailIssue(newIssue, multiSelect = false) { updateDetailIssue(newIssue) {
const { sidebarInfoEndpoint } = newIssue; const { sidebarInfoEndpoint } = newIssue;
if (sidebarInfoEndpoint && newIssue.subscribed === undefined) { if (sidebarInfoEndpoint && newIssue.subscribed === undefined) {
newIssue.setFetchingState('subscriptions', true); newIssue.setFetchingState('subscriptions', true);
...@@ -185,23 +185,9 @@ export default () => { ...@@ -185,23 +185,9 @@ export default () => {
}); });
} }
if (multiSelect) {
boardsStore.toggleMultiSelect(newIssue);
if (boardsStore.detail.issue) {
boardsStore.clearDetailIssue();
return;
}
return;
}
boardsStore.setIssueDetail(newIssue); boardsStore.setIssueDetail(newIssue);
}, },
clearDetailIssue(multiSelect = false) { clearDetailIssue() {
if (multiSelect) {
boardsStore.clearMultiSelect();
}
boardsStore.clearDetailIssue(); boardsStore.clearDetailIssue();
}, },
toggleSubscription(id) { toggleSubscription(id) {
......
...@@ -5,7 +5,6 @@ import ListLabel from './label'; ...@@ -5,7 +5,6 @@ import ListLabel from './label';
import ListAssignee from './assignee'; import ListAssignee from './assignee';
import ListIssue from 'ee_else_ce/boards/models/issue'; import ListIssue from 'ee_else_ce/boards/models/issue';
import { urlParamsToObject } from '~/lib/utils/common_utils'; import { urlParamsToObject } from '~/lib/utils/common_utils';
import flash from '~/flash';
import boardsStore from '../stores/boards_store'; import boardsStore from '../stores/boards_store';
import ListMilestone from './milestone'; import ListMilestone from './milestone';
...@@ -177,53 +176,6 @@ class List { ...@@ -177,53 +176,6 @@ class List {
}); });
} }
addMultipleIssues(issues, listFrom, newIndex) {
let moveBeforeId = null;
let moveAfterId = null;
const listHasIssues = issues.every(issue => this.findIssue(issue.id));
if (!listHasIssues) {
if (newIndex !== undefined) {
if (this.issues[newIndex - 1]) {
moveBeforeId = this.issues[newIndex - 1].id;
}
if (this.issues[newIndex]) {
moveAfterId = this.issues[newIndex].id;
}
this.issues.splice(newIndex, 0, ...issues);
} else {
this.issues.push(...issues);
}
if (this.label) {
issues.forEach(issue => issue.addLabel(this.label));
}
if (this.assignee) {
if (listFrom && listFrom.type === 'assignee') {
issues.forEach(issue => issue.removeAssignee(listFrom.assignee));
}
issues.forEach(issue => issue.addAssignee(this.assignee));
}
if (IS_EE && this.milestone) {
if (listFrom && listFrom.type === 'milestone') {
issues.forEach(issue => issue.removeMilestone(listFrom.milestone));
}
issues.forEach(issue => issue.addMilestone(this.milestone));
}
if (listFrom) {
this.issuesSize += issues.length;
this.updateMultipleIssues(issues, listFrom, moveBeforeId, moveAfterId);
}
}
}
addIssue(issue, listFrom, newIndex) { addIssue(issue, listFrom, newIndex) {
let moveBeforeId = null; let moveBeforeId = null;
let moveAfterId = null; let moveAfterId = null;
...@@ -278,23 +230,6 @@ class List { ...@@ -278,23 +230,6 @@ class List {
}); });
} }
moveMultipleIssues({ issues, oldIndicies, newIndex, moveBeforeId, moveAfterId }) {
oldIndicies.reverse().forEach(index => {
this.issues.splice(index, 1);
});
this.issues.splice(newIndex, 0, ...issues);
gl.boardService
.moveMultipleIssues({
ids: issues.map(issue => issue.id),
fromListId: null,
toListId: null,
moveBeforeId,
moveAfterId,
})
.catch(() => flash(__('Something went wrong while moving issues.')));
}
updateIssueLabel(issue, listFrom, moveBeforeId, moveAfterId) { updateIssueLabel(issue, listFrom, moveBeforeId, moveAfterId) {
gl.boardService gl.boardService
.moveIssue(issue.id, listFrom.id, this.id, moveBeforeId, moveAfterId) .moveIssue(issue.id, listFrom.id, this.id, moveBeforeId, moveAfterId)
...@@ -303,37 +238,10 @@ class List { ...@@ -303,37 +238,10 @@ class List {
}); });
} }
updateMultipleIssues(issues, listFrom, moveBeforeId, moveAfterId) {
gl.boardService
.moveMultipleIssues({
ids: issues.map(issue => issue.id),
fromListId: listFrom.id,
toListId: this.id,
moveBeforeId,
moveAfterId,
})
.catch(() => flash(__('Something went wrong while moving issues.')));
}
findIssue(id) { findIssue(id) {
return this.issues.find(issue => issue.id === id); return this.issues.find(issue => issue.id === id);
} }
removeMultipleIssues(removeIssues) {
const ids = removeIssues.map(issue => issue.id);
this.issues = this.issues.filter(issue => {
const matchesRemove = ids.includes(issue.id);
if (matchesRemove) {
this.issuesSize -= 1;
issue.removeLabel(this.label);
}
return !matchesRemove;
});
}
removeIssue(removeIssue) { removeIssue(removeIssue) {
this.issues = this.issues.filter(issue => { this.issues = this.issues.filter(issue => {
const matchesRemove = removeIssue.id === issue.id; const matchesRemove = removeIssue.id === issue.id;
......
...@@ -48,16 +48,6 @@ export default class BoardService { ...@@ -48,16 +48,6 @@ export default class BoardService {
return boardsStore.moveIssue(id, fromListId, toListId, moveBeforeId, moveAfterId); return boardsStore.moveIssue(id, fromListId, toListId, moveBeforeId, moveAfterId);
} }
moveMultipleIssues({
ids,
fromListId = null,
toListId = null,
moveBeforeId = null,
moveAfterId = null,
}) {
return boardsStore.moveMultipleIssues({ ids, fromListId, toListId, moveBeforeId, moveAfterId });
}
newIssue(id, issue) { newIssue(id, issue) {
return boardsStore.newIssue(id, issue); return boardsStore.newIssue(id, issue);
} }
......
...@@ -11,7 +11,6 @@ import { __ } from '~/locale'; ...@@ -11,7 +11,6 @@ import { __ } from '~/locale';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { mergeUrlParams } from '~/lib/utils/url_utility'; import { mergeUrlParams } from '~/lib/utils/url_utility';
import eventHub from '../eventhub'; import eventHub from '../eventhub';
import { ListType } from '../constants';
const boardsStore = { const boardsStore = {
disabled: false, disabled: false,
...@@ -40,7 +39,6 @@ const boardsStore = { ...@@ -40,7 +39,6 @@ const boardsStore = {
issue: {}, issue: {},
list: {}, list: {},
}, },
multiSelect: { list: [] },
setEndpoints({ boardsEndpoint, listsEndpoint, bulkUpdatePath, boardId, recentBoardsEndpoint }) { setEndpoints({ boardsEndpoint, listsEndpoint, bulkUpdatePath, boardId, recentBoardsEndpoint }) {
const listsEndpointGenerate = `${listsEndpoint}/generate.json`; const listsEndpointGenerate = `${listsEndpoint}/generate.json`;
...@@ -53,6 +51,7 @@ const boardsStore = { ...@@ -53,6 +51,7 @@ const boardsStore = {
recentBoardsEndpoint: `${recentBoardsEndpoint}.json`, recentBoardsEndpoint: `${recentBoardsEndpoint}.json`,
}; };
}, },
create() { create() {
this.state.lists = []; this.state.lists = [];
this.filter.path = getUrlParamsArray().join('&'); this.filter.path = getUrlParamsArray().join('&');
...@@ -135,107 +134,6 @@ const boardsStore = { ...@@ -135,107 +134,6 @@ const boardsStore = {
Object.assign(this.moving, { list, issue }); Object.assign(this.moving, { list, issue });
}, },
moveMultipleIssuesToList({ listFrom, listTo, issues, newIndex }) {
const issueTo = issues.map(issue => listTo.findIssue(issue.id));
const issueLists = _.flatten(issues.map(issue => issue.getLists()));
const listLabels = issueLists.map(list => list.label);
const hasMoveableIssues = _.compact(issueTo).length > 0;
if (!hasMoveableIssues) {
// Check if target list assignee is already present in this issue
if (
listTo.type === ListType.assignee &&
listFrom.type === ListType.assignee &&
issues.some(issue => issue.findAssignee(listTo.assignee))
) {
const targetIssues = issues.map(issue => listTo.findIssue(issue.id));
targetIssues.forEach(targetIssue => targetIssue.removeAssignee(listFrom.assignee));
} else if (listTo.type === 'milestone') {
const currentMilestones = issues.map(issue => issue.milestone);
const currentLists = this.state.lists
.filter(list => list.type === 'milestone' && list.id !== listTo.id)
.filter(list =>
list.issues.some(listIssue => issues.some(issue => listIssue.id === issue.id)),
);
issues.forEach(issue => {
currentMilestones.forEach(milestone => {
issue.removeMilestone(milestone);
});
});
issues.forEach(issue => {
issue.addMilestone(listTo.milestone);
});
currentLists.forEach(currentList => {
issues.forEach(issue => {
currentList.removeIssue(issue);
});
});
listTo.addMultipleIssues(issues, listFrom, newIndex);
} else {
// Add to new lists issues if it doesn't already exist
listTo.addMultipleIssues(issues, listFrom, newIndex);
}
} else {
listTo.updateMultipleIssues(issues, listFrom);
issues.forEach(issue => {
issue.removeLabel(listFrom.label);
});
}
if (listTo.type === ListType.closed && listFrom.type !== ListType.backlog) {
issueLists.forEach(list => {
issues.forEach(issue => {
list.removeIssue(issue);
});
});
issues.forEach(issue => {
issue.removeLabels(listLabels);
});
} else if (listTo.type === ListType.backlog && listFrom.type === ListType.assignee) {
issues.forEach(issue => {
issue.removeAssignee(listFrom.assignee);
});
issueLists.forEach(list => {
issues.forEach(issue => {
list.removeIssue(issue);
});
});
} else if (listTo.type === ListType.backlog && listFrom.type === ListType.milestone) {
issues.forEach(issue => {
issue.removeMilestone(listFrom.milestone);
});
issueLists.forEach(list => {
issues.forEach(issue => {
list.removeIssue(issue);
});
});
} else if (
this.shouldRemoveIssue(listFrom, listTo) &&
this.issuesAreContiguous(listFrom, issues)
) {
listFrom.removeMultipleIssues(issues);
}
},
issuesAreContiguous(list, issues) {
// When there's only 1 issue selected, we can return early.
if (issues.length === 1) return true;
// Create list of ids for issues involved.
const listIssueIds = list.issues.map(issue => issue.id);
const movedIssueIds = issues.map(issue => issue.id);
// Check if moved issue IDs is sub-array
// of source list issue IDs (i.e. contiguous selection).
return listIssueIds.join('|').includes(movedIssueIds.join('|'));
},
moveIssueToList(listFrom, listTo, issue, newIndex) { moveIssueToList(listFrom, listTo, issue, newIndex) {
const issueTo = listTo.findIssue(issue.id); const issueTo = listTo.findIssue(issue.id);
const issueLists = issue.getLists(); const issueLists = issue.getLists();
...@@ -297,17 +195,6 @@ const boardsStore = { ...@@ -297,17 +195,6 @@ const boardsStore = {
list.moveIssue(issue, oldIndex, newIndex, beforeId, afterId); list.moveIssue(issue, oldIndex, newIndex, beforeId, afterId);
}, },
moveMultipleIssuesInList({ list, issues, oldIndicies, newIndex, idArray }) {
const beforeId = parseInt(idArray[newIndex - 1], 10) || null;
const afterId = parseInt(idArray[newIndex + issues.length], 10) || null;
list.moveMultipleIssues({
issues,
oldIndicies,
newIndex,
moveBeforeId: beforeId,
moveAfterId: afterId,
});
},
findList(key, val, type = 'label') { findList(key, val, type = 'label') {
const filteredList = this.state.lists.filter(list => { const filteredList = this.state.lists.filter(list => {
const byType = type const byType = type
...@@ -373,10 +260,6 @@ const boardsStore = { ...@@ -373,10 +260,6 @@ const boardsStore = {
}`; }`;
}, },
generateMultiDragPath(boardId) {
return `${gon.relative_url_root}/-/boards/${boardId ? `${boardId}` : ''}/issues/bulk_move`;
},
all() { all() {
return axios.get(this.state.endpoints.listsEndpoint); return axios.get(this.state.endpoints.listsEndpoint);
}, },
...@@ -426,16 +309,6 @@ const boardsStore = { ...@@ -426,16 +309,6 @@ const boardsStore = {
}); });
}, },
moveMultipleIssues({ ids, fromListId, toListId, moveBeforeId, moveAfterId }) {
return axios.put(this.generateMultiDragPath(this.state.endpoints.boardId), {
from_list_id: fromListId,
to_list_id: toListId,
move_before_id: moveBeforeId,
move_after_id: moveAfterId,
ids,
});
},
newIssue(id, issue) { newIssue(id, issue) {
return axios.post(this.generateIssuesPath(id), { return axios.post(this.generateIssuesPath(id), {
issue, issue,
...@@ -506,25 +379,6 @@ const boardsStore = { ...@@ -506,25 +379,6 @@ const boardsStore = {
setCurrentBoard(board) { setCurrentBoard(board) {
this.state.currentBoard = board; this.state.currentBoard = board;
}, },
toggleMultiSelect(issue) {
const selectedIssueIds = this.multiSelect.list.map(issue => issue.id);
const index = selectedIssueIds.indexOf(issue.id);
if (index === -1) {
this.multiSelect.list.push(issue);
return;
}
this.multiSelect.list = [
...this.multiSelect.list.slice(0, index),
...this.multiSelect.list.slice(index + 1),
];
},
clearMultiSelect() {
this.multiSelect.list = [];
},
}; };
BoardsStoreEE.initEESpecific(boardsStore); BoardsStoreEE.initEESpecific(boardsStore);
......
import 'core-js/es/map'; import 'core-js/es/map';
import 'core-js/es/set'; import 'core-js/es/set';
import { Sortable } from 'sortablejs';
import simulateDrag from './simulate_drag'; import simulateDrag from './simulate_drag';
import simulateInput from './simulate_input'; import simulateInput from './simulate_input';
// Export to global space for rspec to use // Export to global space for rspec to use
window.simulateDrag = simulateDrag; window.simulateDrag = simulateDrag;
window.simulateInput = simulateInput; window.simulateInput = simulateInput;
window.Sortable = Sortable;
...@@ -245,7 +245,6 @@ ...@@ -245,7 +245,6 @@
box-shadow: 0 1px 2px $issue-boards-card-shadow; box-shadow: 0 1px 2px $issue-boards-card-shadow;
line-height: $gl-padding; line-height: $gl-padding;
list-style: none; list-style: none;
position: relative;
&:not(:last-child) { &:not(:last-child) {
margin-bottom: $gl-padding-8; margin-bottom: $gl-padding-8;
...@@ -256,11 +255,6 @@ ...@@ -256,11 +255,6 @@
background-color: $blue-50; background-color: $blue-50;
} }
&.multi-select {
border-color: $blue-200;
background-color: $blue-50;
}
.badge { .badge {
border: 0; border: 0;
outline: 0; outline: 0;
......
...@@ -37,6 +37,7 @@ ...@@ -37,6 +37,7 @@
.documentation { .documentation {
padding: 7px; padding: 7px;
font-size: $gl-font-size-large;
} }
.card.links-card { .card.links-card {
......
...@@ -5,9 +5,6 @@ class Groups::BoardsController < Groups::ApplicationController ...@@ -5,9 +5,6 @@ class Groups::BoardsController < Groups::ApplicationController
include RecordUserLastActivity include RecordUserLastActivity
before_action :assign_endpoint_vars before_action :assign_endpoint_vars
before_action do
push_frontend_feature_flag(:multi_select_board)
end
private private
......
...@@ -7,9 +7,6 @@ class Projects::BoardsController < Projects::ApplicationController ...@@ -7,9 +7,6 @@ class Projects::BoardsController < Projects::ApplicationController
before_action :check_issues_available! before_action :check_issues_available!
before_action :authorize_read_board!, only: [:index, :show] before_action :authorize_read_board!, only: [:index, :show]
before_action :assign_endpoint_vars before_action :assign_endpoint_vars
before_action do
push_frontend_feature_flag(:multi_select_board)
end
private private
......
...@@ -60,12 +60,11 @@ module AtomicInternalId ...@@ -60,12 +60,11 @@ module AtomicInternalId
iid_always_track = Feature.enabled?(:iid_always_track, default_enabled: true) iid_always_track = Feature.enabled?(:iid_always_track, default_enabled: true)
return unless @internal_id_needs_tracking || iid_always_track return unless @internal_id_needs_tracking || iid_always_track
@internal_id_needs_tracking = false
scope_value = internal_id_read_scope(scope) scope_value = internal_id_read_scope(scope)
value = read_attribute(column)
return unless scope_value return unless scope_value
value = read_attribute(column)
if value.present? if value.present?
# The value was set externally, e.g. by the user # The value was set externally, e.g. by the user
# We update the InternalId record to keep track of the greatest value. # We update the InternalId record to keep track of the greatest value.
...@@ -75,6 +74,8 @@ module AtomicInternalId ...@@ -75,6 +74,8 @@ module AtomicInternalId
internal_id_scope_usage, internal_id_scope_usage,
value, value,
init) init)
@internal_id_needs_tracking = false
end end
end end
......
# frozen_string_literal: true
module Git
class ProcessRefChangesService < BaseService
PIPELINE_PROCESS_LIMIT = 4
def execute
changes = params[:changes]
process_changes_by_action(:branch, changes.branch_changes)
process_changes_by_action(:tag, changes.tag_changes)
end
private
def process_changes_by_action(ref_type, changes)
changes_by_action = group_changes_by_action(changes)
changes_by_action.each do |_, changes|
process_changes(ref_type, changes) if changes.any?
end
end
def group_changes_by_action(changes)
changes.group_by do |change|
change_action(change)
end
end
def change_action(change)
return :created if Gitlab::Git.blank_ref?(change[:oldrev])
return :removed if Gitlab::Git.blank_ref?(change[:newrev])
:pushed
end
def process_changes(ref_type, changes)
push_service_class = push_service_class_for(ref_type)
changes.each do |change|
push_service_class.new(
project,
current_user,
change: change,
push_options: params[:push_options],
create_pipelines: change[:index] < PIPELINE_PROCESS_LIMIT || Feature.enabled?(:git_push_create_all_pipelines, project)
).execute
end
end
def push_service_class_for(ref_type)
return Git::TagPushService if ref_type == :tag
Git::BranchPushService
end
end
end
...@@ -56,7 +56,7 @@ module Issues ...@@ -56,7 +56,7 @@ module Issues
handle_milestone_change(issue) handle_milestone_change(issue)
added_mentions = issue.mentioned_users - old_mentioned_users added_mentions = issue.mentioned_users(current_user) - old_mentioned_users
if added_mentions.present? if added_mentions.present?
notification_service.async.new_mentions_in_issue(issue, added_mentions, current_user) notification_service.async.new_mentions_in_issue(issue, added_mentions, current_user)
......
...@@ -69,7 +69,8 @@ module MergeRequests ...@@ -69,7 +69,8 @@ module MergeRequests
) )
end end
added_mentions = merge_request.mentioned_users - old_mentioned_users added_mentions = merge_request.mentioned_users(current_user) - old_mentioned_users
if added_mentions.present? if added_mentions.present?
notification_service.async.new_mentions_in_merge_request( notification_service.async.new_mentions_in_merge_request(
merge_request, merge_request,
......
...@@ -5,7 +5,7 @@ module Notes ...@@ -5,7 +5,7 @@ module Notes
def execute(note) def execute(note)
return note unless note.editable? return note unless note.editable?
old_mentioned_users = note.mentioned_users.to_a old_mentioned_users = note.mentioned_users(current_user).to_a
note.update(params.merge(updated_by: current_user)) note.update(params.merge(updated_by: current_user))
......
- page_title @path.split("/").reverse.map(&:humanize) - page_title @path.split("/").reverse.map(&:humanize)
- @content_class = "limit-container-width" unless fluid_layout
.documentation.md.prepend-top-default .documentation.md.prepend-top-default
= markdown @markdown = markdown @markdown
...@@ -3,8 +3,6 @@ ...@@ -3,8 +3,6 @@
class PostReceive class PostReceive
include ApplicationWorker include ApplicationWorker
PIPELINE_PROCESS_LIMIT = 4
def perform(gl_repository, identifier, changes, push_options = {}) def perform(gl_repository, identifier, changes, push_options = {})
project, repo_type = Gitlab::GlRepository.parse(gl_repository) project, repo_type = Gitlab::GlRepository.parse(gl_repository)
...@@ -49,8 +47,7 @@ class PostReceive ...@@ -49,8 +47,7 @@ class PostReceive
expire_caches(post_received, post_received.project.repository) expire_caches(post_received, post_received.project.repository)
enqueue_repository_cache_update(post_received) enqueue_repository_cache_update(post_received)
process_changes(Git::BranchPushService, project, user, push_options, changes.branch_changes) process_ref_changes(project, user, push_options: push_options, changes: changes)
process_changes(Git::TagPushService, project, user, push_options, changes.tag_changes)
update_remote_mirrors(post_received) update_remote_mirrors(post_received)
after_project_changes_hooks(project, user, changes.refs, changes.repository_data) after_project_changes_hooks(project, user, changes.refs, changes.repository_data)
end end
...@@ -75,18 +72,10 @@ class PostReceive ...@@ -75,18 +72,10 @@ class PostReceive
) )
end end
def process_changes(service_class, project, user, push_options, changes) def process_ref_changes(project, user, params = {})
return if changes.empty? return unless params[:changes].any?
changes.each do |change| Git::ProcessRefChangesService.new(project, user, params).execute
service_class.new(
project,
user,
change: change,
push_options: push_options,
create_pipelines: change[:index] < PIPELINE_PROCESS_LIMIT || Feature.enabled?(:git_push_create_all_pipelines, project)
).execute
end
end end
def update_remote_mirrors(post_received) def update_remote_mirrors(post_received)
......
---
title: Fix notifications for private group mentions in Notes, Issues, and Merge Requests
merge_request: 18183
author:
type: fixed
---
title: Moves the license compliance reports to the Backend
merge_request: 17905
author:
type: other
---
title: Improve UI of documentation under /help
merge_request: 18331
author:
type: changed
...@@ -258,6 +258,7 @@ The following documentation relates to the DevOps **Package** stage: ...@@ -258,6 +258,7 @@ The following documentation relates to the DevOps **Package** stage:
|:----------------------------------------------------------------|:-------------------------------------------------------| |:----------------------------------------------------------------|:-------------------------------------------------------|
| [Container Registry](user/packages/container_registry/index.md) | The GitLab Container Registry enables every project in GitLab to have its own space to store [Docker](https://www.docker.com/) images. | | [Container Registry](user/packages/container_registry/index.md) | The GitLab Container Registry enables every project in GitLab to have its own space to store [Docker](https://www.docker.com/) images. |
| [Dependency Proxy](user/packages/dependency_proxy/index.md) **(PREMIUM)** | The GitLab Dependency Proxy sets up a local proxy for frequently used upstream images/packages. | | [Dependency Proxy](user/packages/dependency_proxy/index.md) **(PREMIUM)** | The GitLab Dependency Proxy sets up a local proxy for frequently used upstream images/packages. |
| [Conan Repository](user/packages/conan_repository/index.md) **(PREMIUM)** | The GitLab Conan Repository enables every project in GitLab to have its own space to store [Conan](https://conan.io/) packages. |
| [Maven Repository](user/packages/maven_repository/index.md) **(PREMIUM)** | The GitLab Maven Repository enables every project in GitLab to have its own space to store [Maven](https://maven.apache.org/) packages. | | [Maven Repository](user/packages/maven_repository/index.md) **(PREMIUM)** | The GitLab Maven Repository enables every project in GitLab to have its own space to store [Maven](https://maven.apache.org/) packages. |
| [NPM Registry](user/packages/npm_registry/index.md) **(PREMIUM)** | The GitLab NPM Registry enables every project in GitLab to have its own space to store [NPM](https://www.npmjs.com/) packages. | | [NPM Registry](user/packages/npm_registry/index.md) **(PREMIUM)** | The GitLab NPM Registry enables every project in GitLab to have its own space to store [NPM](https://www.npmjs.com/) packages. |
......
...@@ -281,6 +281,7 @@ You can keep track of the progress to include the missing items in: ...@@ -281,6 +281,7 @@ You can keep track of the progress to include the missing items in:
| [Container Registry](../../packages/container_registry.md) | Yes | No | | [Container Registry](../../packages/container_registry.md) | Yes | No |
| [NPM Registry](../../../user/packages/npm_registry/index.md) | No | No | | [NPM Registry](../../../user/packages/npm_registry/index.md) | No | No |
| [Maven Packages](../../../user/packages/maven_repository/index.md) | No | No | | [Maven Packages](../../../user/packages/maven_repository/index.md) | No | No |
| [Conan Packages](../../../user/packages/conan_repository/index.md) | No | No |
| [External merge request diffs](../../merge_request_diffs.md) | No, if they are on-disk | No | | [External merge request diffs](../../merge_request_diffs.md) | No, if they are on-disk | No |
| Content in object storage ([track progress](https://gitlab.com/groups/gitlab-org/-/epics/1526)) | No | No | | Content in object storage ([track progress](https://gitlab.com/groups/gitlab-org/-/epics/1526)) | No | No |
......
...@@ -8,6 +8,7 @@ The Packages feature allows GitLab to act as a repository for the following: ...@@ -8,6 +8,7 @@ The Packages feature allows GitLab to act as a repository for the following:
| Software repository | Description | Available in GitLab version | | Software repository | Description | Available in GitLab version |
| ------------------- | ----------- | --------------------------- | | ------------------- | ----------- | --------------------------- |
| [Conan Repository](../../user/packages/conan_repository/index.md) | The GitLab Conan Repository enables every project in GitLab to have its own space to store [Conan](https://conan.io/) packages. | 12.4+ |
| [Maven Repository](../../user/packages/maven_repository/index.md) | The GitLab Maven Repository enables every project in GitLab to have its own space to store [Maven](https://maven.apache.org/) packages. | 11.3+ | | [Maven Repository](../../user/packages/maven_repository/index.md) | The GitLab Maven Repository enables every project in GitLab to have its own space to store [Maven](https://maven.apache.org/) packages. | 11.3+ |
| [NPM Registry](../../user/packages/npm_registry/index.md) | The GitLab NPM Registry enables every project in GitLab to have its own space to store [NPM](https://www.npmjs.com/) packages. | 11.7+ | | [NPM Registry](../../user/packages/npm_registry/index.md) | The GitLab NPM Registry enables every project in GitLab to have its own space to store [NPM](https://www.npmjs.com/) packages. | 11.7+ |
......
...@@ -190,6 +190,7 @@ according to each stage (Verify, Package, Release). ...@@ -190,6 +190,7 @@ according to each stage (Verify, Package, Release).
- Store Docker images with [Container Registry](../../user/packages/container_registry/index.md). - Store Docker images with [Container Registry](../../user/packages/container_registry/index.md).
- Store NPM packages with [NPM Registry](../../user/packages/npm_registry/index.md). **(PREMIUM)** - Store NPM packages with [NPM Registry](../../user/packages/npm_registry/index.md). **(PREMIUM)**
- Store Maven artifacts with [Maven Repository](../../user/packages/maven_repository/index.md). **(PREMIUM)** - Store Maven artifacts with [Maven Repository](../../user/packages/maven_repository/index.md). **(PREMIUM)**
- Store Conan packages with [Conan Repository](../../user/packages/conan_repository/index.md). **(PREMIUM)**
1. **Release**: 1. **Release**:
- Continuous Deployment, automatically deploying your app to production. - Continuous Deployment, automatically deploying your app to production.
- Continuous Delivery, manually click to deploy your app to production. - Continuous Delivery, manually click to deploy your app to production.
......
...@@ -374,17 +374,29 @@ The following GitLab features are used among others: ...@@ -374,17 +374,29 @@ The following GitLab features are used among others:
## Testing ## Testing
We treat documentation as code, and so use tests to maintain the standards and quality of the docs. We treat documentation as code, and so use tests in our CI pipeline to maintain the
The current tests are: standards and quality of the docs. The current tests, which run in CI jobs when a
merge request with new or changed docs is submitted, are:
1. `docs lint`: Check that all internal (relative) links work correctly and
that all cURL examples in API docs use the full switches. It's recommended - [`docs lint`](https://gitlab.com/gitlab-org/gitlab/blob/master/.gitlab/ci/docs.gitlab-ci.yml#L48):
to [check locally](#previewing-the-changes-live) before pushing to GitLab by executing the command Runs several tests on the content of the docs themselves:
`bundle exec nanoc check internal_links` on your local - [`lint-doc.sh` script](https://gitlab.com/gitlab-org/gitlab/blob/master/scripts/lint-doc.sh)
[`gitlab-docs`](https://gitlab.com/gitlab-org/gitlab-docs) directory. In addition, checks that:
`docs-lint` also runs [`markdownlint`](#markdownlint) to ensure the - All cURL examples use the long flags (ex: `--header`, not `-H`).
markdown is consistent across all documentation. - The `CHANGELOG.md` does not contain duplicate versions.
1. In a full pipeline, tests for [`/help`](#gitlab-help-tests). - No files in `doc/` are executable.
- No new `README.md` was added.
- [`markdownlint`](#markdownlint).
- Nanoc tests, which you can [run locally](#previewing-the-changes-live) before
pushing to GitLab by executing the command `bundle exec nanoc check internal_links`
(or `internal_anchors`) on your local [`gitlab-docs`](https://gitlab.com/gitlab-org/gitlab-docs) directory:
- [`internal_links`](https://gitlab.com/gitlab-org/gitlab/blob/master/.gitlab/ci/docs.gitlab-ci.yml#L67)
checks that all internal links (ex: `[link](../index.md)`) are valid.
- [`internal_anchors`](https://gitlab.com/gitlab-org/gitlab/blob/master/.gitlab/ci/docs.gitlab-ci.yml#L69)
checks that all internal anchors (ex: `[link](../index.md#internal_anchor)`)
are valid.
- If any code or the `doc/README.md` file is changed, a full pipeline will run, which
runs tests for [`/help`](#gitlab-help-tests).
### Linting ### Linting
...@@ -490,7 +502,10 @@ four repos that are the sources for <https://docs.gitlab.com>: ...@@ -490,7 +502,10 @@ four repos that are the sources for <https://docs.gitlab.com>:
- <https://gitlab.com/charts/gitlab/blob/master/.markdownlint.json> - <https://gitlab.com/charts/gitlab/blob/master/.markdownlint.json>
By default all rules are enabled, so the configuration file is used to disable unwanted By default all rules are enabled, so the configuration file is used to disable unwanted
rules, and also to configure optional parameters for enabled rules as needed. rules, and also to configure optional parameters for enabled rules as needed. You can
also check [the issue](https://gitlab.com/gitlab-org/gitlab-foss/issues/64352) that
tracked the changes required to implement these rules, and details which rules were
on or off when `markdownlint` was enabled on the docs.
## Danger Bot ## Danger Bot
......
# Access for enabling a feature flag in production # Feature flag controls
In order to be able to turn on/off features behind feature flags in any of the ## Access
To be able to turn on/off features behind feature flags in any of the
GitLab Inc. provided environments such as staging and production, you need to GitLab Inc. provided environments such as staging and production, you need to
have access to the chatops bot. Chatops bot is currently running on the ops instance, have access to the [Chatops](../chatops_on_gitlabcom.md) bot. The Chatops bot
which is different from <https://gitlab.com> or <https://dev.gitlab.org>. is currently running on the ops instance, which is different from <https://gitlab.com> or <https://dev.gitlab.org>.
Follow the Chatops document to [request access](../chatops_on_gitlabcom.md#requesting-access). Follow the Chatops document to [request access](../chatops_on_gitlabcom.md#requesting-access).
...@@ -14,6 +16,19 @@ run: ...@@ -14,6 +16,19 @@ run:
/chatops run feature --help /chatops run feature --help
``` ```
## Where to run commands
To increase visibility, we recommend that GitLab team members run feature flag
related Chatops commands within certain slack channels based on the environment
and related feature. For the [staging](https://staging.gitlab.com)
and [development](https://dev.gitlab.org) environments of GitLab.com,
the commands should run in a channel for the stage the feature is relevant too.
For example, use the `#s_monitor` channel for features developed by the
Monitor stage, Health group.
For all production environment Chatops commands, use the `#production` channel.
## Rolling out changes ## Rolling out changes
When the changes are deployed to the environments it is time to start When the changes are deployed to the environments it is time to start
...@@ -28,7 +43,7 @@ easier to measure the impact of both separately. ...@@ -28,7 +43,7 @@ easier to measure the impact of both separately.
GitLab's feature library (using GitLab's feature library (using
[Flipper](https://github.com/jnunemaker/flipper), and covered in the [Feature [Flipper](https://github.com/jnunemaker/flipper), and covered in the [Feature
Flags process](process.md) guide) supports rolling out changes to a percentage of Flags process](process.md) guide) supports rolling out changes to a percentage of
users. This in turn can be controlled using [GitLab chatops](../../ci/chatops/README.md). users. This in turn can be controlled using [GitLab Chatops](../../ci/chatops/README.md).
For an up to date list of feature flag commands please see [the source For an up to date list of feature flag commands please see [the source
code](https://gitlab.com/gitlab-com/chatops/blob/master/lib/chatops/commands/feature.rb). code](https://gitlab.com/gitlab-com/chatops/blob/master/lib/chatops/commands/feature.rb).
...@@ -37,7 +52,7 @@ Note that all the examples in that file must be preceded by ...@@ -37,7 +52,7 @@ Note that all the examples in that file must be preceded by
If you get an error "Whoops! This action is not allowed. This incident If you get an error "Whoops! This action is not allowed. This incident
will be reported." that means your Slack account is not allowed to will be reported." that means your Slack account is not allowed to
change feature flags or you do not [have access](#access-for-enabling-a-feature-flag-in-production). change feature flags or you do not [have access](#access).
### Enabling feature for internal testing ### Enabling feature for internal testing
...@@ -64,7 +79,7 @@ there for any exceptions while testing your feature after enabling the feature f ...@@ -64,7 +79,7 @@ there for any exceptions while testing your feature after enabling the feature f
Once you are confident enough that these environments are in a good state with your Once you are confident enough that these environments are in a good state with your
feature enabled, you can roll out the change to GitLab.com. feature enabled, you can roll out the change to GitLab.com.
## Enabling feature for GitLab.com ### Enabling a feature for GitLab.com
Similar to above, to enable a feature for 25% of all users, run the following in Similar to above, to enable a feature for 25% of all users, run the following in
Slack: Slack:
...@@ -114,10 +129,17 @@ merge request has to be picked into a stable branch, make sure to also add the ...@@ -114,10 +129,17 @@ merge request has to be picked into a stable branch, make sure to also add the
appropriate "Pick into X" label (e.g. "Pick into XX.X"). appropriate "Pick into X" label (e.g. "Pick into XX.X").
See [the process document](process.md#including-a-feature-behind-feature-flag-in-the-final-release) for further details. See [the process document](process.md#including-a-feature-behind-feature-flag-in-the-final-release) for further details.
When a feature gate has been removed from the code base, the value still exists When a feature gate has been removed from the code base, the feature
in the database. record still exists in the database that the flag was deployed too.
This can be removed through ChatOps: The record can be deleted once the MR is deployed to each environment:
```sh
/chatops run feature delete some_feature --dev
/chatops run feature delete some_feature --staging
``` ```
Then, you can delete it from production after the MR is deployed to prod:
```sh
/chatops run feature delete some_feature /chatops run feature delete some_feature
``` ```
# GitLab Conan Repository **(PREMIUM)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/8248) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.4.
With the GitLab Conan Repository, every
project can have its own space to store Conan packages.
![GitLab Conan Repository](img/conan_package_view.png)
## Enabling the Conan Repository
NOTE: **Note:**
This option is available only if your GitLab administrator has
[enabled support for the Conan Repository](../../../administration/packages/index.md).**(PREMIUM ONLY)**
After the Conan Repository is enabled, it will be available for all new projects
by default. To enable it for existing projects, or if you want to disable it:
1. Navigate to your project's **Settings > General > Permissions**.
1. Find the Packages feature and enable or disable it.
1. Click on **Save changes** for the changes to take effect.
You should then be able to see the **Packages** section on the left sidebar.
Before proceeding to authenticating with the GitLab Conan Repository, you should
get familiar with the package naming convention.
## Authenticating to the GitLab Conan Repository
You will need to generate a [personal access token](../../../user/profile/personal_access_tokens.md) for repository authentication.
Now you can run conan commands using your token.
`CONAN_LOGIN_USERNAME=<gitlab-username> CONAN_PASSWORD=<personal_access_token> conan upload Hello/0.2@user/channel --remote=gitlab`
`CONAN_LOGIN_USERNAME=<gitlab-username> CONAN_PASSWORD=<personal_access_token> conan search Hello* --all --remote=gitlab`
Alternatively, you can set the `CONAN_LOGIN_USERNAME` and `CONAN_PASSWORD` in your local conan config to be used when connecting to the `gitlab` remote. The examples here show the username and password inline.
Next, you'll need to set your Conan remote to point to the GitLab Package Registry.
## Setting the Conan remote to the GitLab Package Registry
After you authenticate to the [GitLab Conan Repository](#authenticating-to-the-gitlab-conan-repository),
you can set the Conan remote:
```sh
conan remote add gitlab https://gitlab.example.com/api/v4/packages/conan
```
Once the remote is set, you can use the remote when running Conan commands:
```sh
conan search Hello* --all --remote=gitlab
```
## Supported CLI commands
The GitLab Conan repository supports the following Conan CLI commands:
- `conan upload`: Upload your recipe and package files to the GitLab Package Registry.
- `conan install`: Install a conan package from the GitLab Package Registry, this includes using the `conan.txt` file.
- `conan search`: Search the GitLab Package Registry for public packages, and private packages you have permission to view.
- `conan info`: View the info on a given package from the GitLab Package Registry.
- `conan remove`: Delete the package from the GitLab Package Registry.
## Uploading a package
First you need to [create your Conan package locally](https://docs.conan.io/en/latest/creating_packages/getting_started.html). In order to work with the GitLab Package Registry, a specific [naming convention](#package-recipe-naming-convention) must be followed.
Ensure you have a project created on GitLab and that the personal access token you are using has the correct permissions for write access to the container registry by selecting the `api` [scope](../../../user/profile/personal_access_tokens.md#limiting-scopes-of-a-personal-access-token).
You can upload your package to the GitLab Package Registry using the `conan upload` command:
```sh
CONAN_LOGIN_USERNAME=<gitlab-username> CONAN_PASSWORD=<personal_access_token> conan upload Hello/0.1@my-group+my-project/beta --all --remote=gitlab
```
### Package recipe naming convention
Standard Conan recipe convention looks like `package_name/version@username/channel`.
**Recipe usernames must be the `+` separated project path**. The package
name may be anything, but it is preferred that the project name be used unless
it is not possible due to a naming collision. For example:
| Project | Package | Supported |
| ---------------------------------- | ----------------------------------------------- | --------- |
| `foo/bar` | `my-package/1.0.0@foo+bar/stable` | Yes |
| `foo/bar-baz/buz` | `my-package/1.0.0@foo+bar-baz+buz/stable` | Yes |
| `gitlab-org/gitlab-ce` | `my-package/1.0.0@gitlab-org+gitlab-ce/stable` | Yes |
| `gitlab-org/gitlab-ce` | `my-package/1.0.0@foo/stable` | No |
NOTE: **Note:**
A future iteration will extend support to [project and group level](https://gitlab.com/gitlab-org/gitlab/issues/11679) remotes which will allow for more flexible naming conventions.
## Installing a package
Add the conan package to the `[requires]` section of your `conan.txt` file and they will be installed when you run `conan install` within your project.
## Removing a package
There are two ways to remove a Conan package from the GitLab Package Registry.
- **Using the Conan client in the command line:**
```sh
CONAN_LOGIN_USERNAME=<gitlab-username> CONAN_PASSWORD=<personal_access_token> conan remove Hello/0.2@user/channel -r gitlab
```
NOTE: **Note:**
This command will remove all recipe and binary package files from the Package Registry.
- **GitLab project interface**: in the packages view of your project page, you can delete packages by clicking the red trash icons.
## Searching the GitLab Package Registry for Conan packages
The `conan search` command can be run searching by full or partial package name, or by exact recipe.
To search using a partial name, use the wildcard symbol `*`, which should be placed at the end of your search (e.g., `my-packa*`):
```sh
CONAN_LOGIN_USERNAME=<gitlab-username> CONAN_PASSWORD=<personal_access_token> conan search Hello --all --remote=gitlab
CONAN_LOGIN_USERNAME=<gitlab-username> CONAN_PASSWORD=<personal_access_token> conan search He* --all --remote=gitlab
CONAN_LOGIN_USERNAME=<gitlab-username> CONAN_PASSWORD=<personal_access_token> conan search Hello/1.0.0@my-group+my-project/stable --all --remote=gitlab
```
The scope of your search will include all projects you have permission to access, this includes your private projects as well as all public projects.
## Fetching Conan package info from the GitLab Package Registry
The `conan info` command will return info about a given package:
```sh
CONAN_LOGIN_USERNAME=<gitlab-username> CONAN_PASSWORD=<personal_access_token> conan info Hello/1.0.0@my-group+my-project/stable -r gitlab
```
...@@ -10,6 +10,7 @@ The Packages feature allows GitLab to act as a repository for the following: ...@@ -10,6 +10,7 @@ The Packages feature allows GitLab to act as a repository for the following:
| ------------------- | ----------- | --------------------------- | | ------------------- | ----------- | --------------------------- |
| [Container Registry](container_registry/index.md) | The GitLab Container Registry enables every project in GitLab to have its own space to store [Docker](https://www.docker.com/) images. | 8.8+ | | [Container Registry](container_registry/index.md) | The GitLab Container Registry enables every project in GitLab to have its own space to store [Docker](https://www.docker.com/) images. | 8.8+ |
| [Dependency Proxy](dependency_proxy/index.md) **(PREMIUM)** | The GitLab Dependency Proxy sets up a local proxy for frequently used upstream images/packages. | 11.11+ | | [Dependency Proxy](dependency_proxy/index.md) **(PREMIUM)** | The GitLab Dependency Proxy sets up a local proxy for frequently used upstream images/packages. | 11.11+ |
| [Conan Repository](conan_repository/index.md) **(PREMIUM)** | The GitLab Conan Repository enables every project in GitLab to have its own space to store [Conan](https://conan.io/) packages. | 12.4+ |
| [Maven Repository](maven_repository/index.md) **(PREMIUM)** | The GitLab Maven Repository enables every project in GitLab to have its own space to store [Maven](https://maven.apache.org/) packages. | 11.3+ | | [Maven Repository](maven_repository/index.md) **(PREMIUM)** | The GitLab Maven Repository enables every project in GitLab to have its own space to store [Maven](https://maven.apache.org/) packages. | 11.3+ |
| [NPM Registry](npm_registry/index.md) **(PREMIUM)** | The GitLab NPM Registry enables every project in GitLab to have its own space to store [NPM](https://www.npmjs.com/) packages. | 11.7+ | | [NPM Registry](npm_registry/index.md) **(PREMIUM)** | The GitLab NPM Registry enables every project in GitLab to have its own space to store [NPM](https://www.npmjs.com/) packages. | 11.7+ |
......
...@@ -76,8 +76,8 @@ The following table depicts the various user permission levels in a project. ...@@ -76,8 +76,8 @@ The following table depicts the various user permission levels in a project.
| See a list of merge requests | | ✓ | ✓ | ✓ | ✓ | | See a list of merge requests | | ✓ | ✓ | ✓ | ✓ |
| View project statistics | | ✓ | ✓ | ✓ | ✓ | | View project statistics | | ✓ | ✓ | ✓ | ✓ |
| View Error Tracking list | | ✓ | ✓ | ✓ | ✓ | | View Error Tracking list | | ✓ | ✓ | ✓ | ✓ |
| Pull from [Maven repository](packages/maven_repository/index.md) or [NPM registry](packages/npm_registry/index.md) **(PREMIUM)** | | ✓ | ✓ | ✓ | ✓ | | Pull from [Conan repository](packages/conan_repository/index.md), [Maven repository](packages/maven_repository/index.md), or [NPM registry](packages/npm_registry/index.md) **(PREMIUM)** | | ✓ | ✓ | ✓ | ✓ |
| Publish to [Maven repository](packages/maven_repository/index.md) or [NPM registry](packages/npm_registry/index.md) **(PREMIUM)** | | | ✓ | ✓ | ✓ | | Publish to [Conan repository](packages/conan_repository/index.md), [Maven repository](packages/maven_repository/index.md), or [NPM registry](packages/npm_registry/index.md) **(PREMIUM)** | | | ✓ | ✓ | ✓ |
| Upload [Design Management](project/issues/design_management.md) files **(PREMIUM)** | | | ✓ | ✓ | ✓ | | Upload [Design Management](project/issues/design_management.md) files **(PREMIUM)** | | | ✓ | ✓ | ✓ |
| Create new branches | | | ✓ | ✓ | ✓ | | Create new branches | | | ✓ | ✓ | ✓ |
| Push to non-protected branches | | | ✓ | ✓ | ✓ | | Push to non-protected branches | | | ✓ | ✓ | ✓ |
......
...@@ -95,6 +95,7 @@ When you create a project in GitLab, you'll have access to a large number of ...@@ -95,6 +95,7 @@ When you create a project in GitLab, you'll have access to a large number of
- [Releases](releases/index.md): a way to track deliverables in your project as snapshot in time of - [Releases](releases/index.md): a way to track deliverables in your project as snapshot in time of
the source, build output, and other metadata or artifacts the source, build output, and other metadata or artifacts
associated with a released version of your code. associated with a released version of your code.
- [Conan packages](../packages/conan_repository/index.md): your private Conan repository in GitLab. **(PREMIUM)**
- [Maven packages](../packages/maven_repository/index.md): your private Maven repository in GitLab. **(PREMIUM)** - [Maven packages](../packages/maven_repository/index.md): your private Maven repository in GitLab. **(PREMIUM)**
- [NPM packages](../packages/npm_registry/index.md): your private NPM package registry in GitLab. **(PREMIUM)** - [NPM packages](../packages/npm_registry/index.md): your private NPM package registry in GitLab. **(PREMIUM)**
- [Code owners](code_owners.md): specify code owners for certain files **(STARTER)** - [Code owners](code_owners.md): specify code owners for certain files **(STARTER)**
......
...@@ -180,18 +180,6 @@ These are shortcuts to your last 4 visited boards. ...@@ -180,18 +180,6 @@ These are shortcuts to your last 4 visited boards.
When you're revisiting an issue board in a project or group with multiple boards, When you're revisiting an issue board in a project or group with multiple boards,
GitLab will automatically load the last board you visited. GitLab will automatically load the last board you visited.
### Multi-select Issue Cards
As the name suggest, multi-select issue cards allows more than one issue card
to be dragged and dropped across different lists. This becomes helpful while
moving and grooming a lot of issues at once.
You can multi-select an issue card by pressing `CTRL` + `Left mouse click` on
Windows or `CMD` + `Left mouse click` on MacOS. Once done, start by dragging one
of the issue card you have selected and drop it in the new list you want.
![Multi-select Issue Cards](img/issue_boards_multi_select.png)
### Configurable Issue Boards **(STARTER)** ### Configurable Issue Boards **(STARTER)**
> Introduced in [GitLab Starter Edition 10.2](https://about.gitlab.com/2017/11/22/gitlab-10-2-released/#issue-boards-configuration). > Introduced in [GitLab Starter Edition 10.2](https://about.gitlab.com/2017/11/22/gitlab-10-2-released/#issue-boards-configuration).
......
...@@ -11,7 +11,7 @@ requests within GitLab. ...@@ -11,7 +11,7 @@ requests within GitLab.
## Overview ## Overview
Time Tracking allows you: Time Tracking allows you to:
- Record the time spent working on an issue or a merge request. - Record the time spent working on an issue or a merge request.
- Add an estimate of the amount of time needed to complete an issue or a merge - Add an estimate of the amount of time needed to complete an issue or a merge
......
...@@ -14842,9 +14842,6 @@ msgid_plural "Showing %d events" ...@@ -14842,9 +14842,6 @@ msgid_plural "Showing %d events"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
msgid "Showing %{pageSize} of %{total} issues"
msgstr ""
msgid "Showing Latest Version" msgid "Showing Latest Version"
msgstr "" msgstr ""
...@@ -15100,12 +15097,6 @@ msgstr "" ...@@ -15100,12 +15097,6 @@ msgstr ""
msgid "Something went wrong while merging this merge request. Please try again." msgid "Something went wrong while merging this merge request. Please try again."
msgstr "" msgstr ""
msgid "Something went wrong while moving issues."
msgstr ""
msgid "Something went wrong while performing the action."
msgstr ""
msgid "Something went wrong while reopening the %{issuable}. Please try again later" msgid "Something went wrong while reopening the %{issuable}. Please try again later"
msgstr "" msgstr ""
......
# frozen_string_literal: true
require 'spec_helper'
describe 'Multi Select Issue', :js do
include DragTo
let(:group) { create(:group, :nested) }
let(:project) { create(:project, :public, namespace: group) }
let(:board) { create(:board, project: project) }
let(:user) { create(:user) }
def drag(selector: '.board-list', list_from_index: 1, from_index: 0, to_index: 0, list_to_index: 1, duration: 1000)
drag_to(selector: selector,
scrollable: '#board-app',
list_from_index: list_from_index,
from_index: from_index,
to_index: to_index,
list_to_index: list_to_index,
duration: duration)
end
def wait_for_board_cards(board_number, expected_cards)
page.within(find(".board:nth-child(#{board_number})")) do
expect(page.find('.board-header')).to have_content(expected_cards.to_s)
expect(page).to have_selector('.board-card', count: expected_cards)
end
end
def multi_select(selector, action = 'select')
element = page.find(selector)
script = "var el = document.querySelector('#{selector}');"
script += "var mousedown = new MouseEvent('mousedown', { button: 0, bubbles: true });"
script += "var mouseup = new MouseEvent('mouseup', { ctrlKey: true, button: 0, bubbles:true });"
script += "el.dispatchEvent(mousedown); el.dispatchEvent(mouseup);"
script += "Sortable.utils.#{action}(el);"
page.execute_script(script, element)
end
before do
project.add_maintainer(user)
sign_in(user)
end
context 'with lists' do
let(:label1) { create(:label, project: project, name: 'Label 1', description: 'Test') }
let(:label2) { create(:label, project: project, name: 'Label 2', description: 'Test') }
let!(:list1) { create(:list, board: board, label: label1, position: 0) }
let!(:list2) { create(:list, board: board, label: label2, position: 1) }
let!(:issue1) { create(:labeled_issue, project: project, title: 'Issue 1', description: '', assignees: [user], labels: [label1], relative_position: 1) }
let!(:issue2) { create(:labeled_issue, project: project, title: 'Issue 2', description: '', author: user, labels: [label1], relative_position: 2) }
let!(:issue3) { create(:labeled_issue, project: project, title: 'Issue 3', description: '', labels: [label1], relative_position: 3) }
let!(:issue4) { create(:labeled_issue, project: project, title: 'Issue 4', description: '', labels: [label1], relative_position: 4) }
let!(:issue5) { create(:labeled_issue, project: project, title: 'Issue 5', description: '', labels: [label1], relative_position: 5) }
before do
visit project_board_path(project, board)
wait_for_requests
end
it 'moves multiple issues to another list', :js do
multi_select('.board-card:nth-child(1)')
multi_select('.board-card:nth-child(2)')
drag(list_from_index: 1, list_to_index: 2)
wait_for_requests
page.within(all('.js-board-list')[2]) do
expect(find('.board-card:nth-child(1)')).to have_content(issue1.title)
expect(find('.board-card:nth-child(2)')).to have_content(issue2.title)
end
end
it 'maintains order when moved', :js do
multi_select('.board-card:nth-child(3)')
multi_select('.board-card:nth-child(2)')
multi_select('.board-card:nth-child(1)')
drag(list_from_index: 1, list_to_index: 2)
wait_for_requests
page.within(all('.js-board-list')[2]) do
expect(find('.board-card:nth-child(1)')).to have_content(issue1.title)
expect(find('.board-card:nth-child(2)')).to have_content(issue2.title)
expect(find('.board-card:nth-child(3)')).to have_content(issue3.title)
end
end
it 'move issues in the same list', :js do
multi_select('.board-card:nth-child(3)')
multi_select('.board-card:nth-child(4)')
drag(list_from_index: 1, list_to_index: 1, from_index: 2, to_index: 4)
wait_for_requests
page.within(all('.js-board-list')[1]) do
expect(find('.board-card:nth-child(1)')).to have_content(issue1.title)
expect(find('.board-card:nth-child(2)')).to have_content(issue2.title)
expect(find('.board-card:nth-child(3)')).to have_content(issue5.title)
expect(find('.board-card:nth-child(4)')).to have_content(issue3.title)
expect(find('.board-card:nth-child(5)')).to have_content(issue4.title)
end
end
it 'adds label when issues are moved to different card', :js do
page.within(all('.js-board-list')[1]) do
expect(find('.board-card:nth-child(1)')).not_to have_content(label2.title)
expect(find('.board-card:nth-child(2)')).not_to have_content(label2.title)
end
multi_select('.board-card:nth-child(1)')
multi_select('.board-card:nth-child(2)')
drag(list_from_index: 1, list_to_index: 2)
wait_for_requests
page.within(all('.js-board-list')[2]) do
expect(find('.board-card:nth-child(1)')).to have_content(label2.title)
expect(find('.board-card:nth-child(2)')).to have_content(label2.title)
end
end
end
end
...@@ -67,16 +67,6 @@ describe('Board card', () => { ...@@ -67,16 +67,6 @@ describe('Board card', () => {
expect(vm.issueDetailVisible).toBe(true); expect(vm.issueDetailVisible).toBe(true);
}); });
it("returns false when multiSelect doesn't contain issue", () => {
expect(vm.multiSelectVisible).toBe(false);
});
it('returns true when multiSelect contains issue', () => {
boardsStore.multiSelect.list = [vm.issue];
expect(vm.multiSelectVisible).toBe(true);
});
it('adds user-can-drag class if not disabled', () => { it('adds user-can-drag class if not disabled', () => {
expect(vm.$el.classList.contains('user-can-drag')).toBe(true); expect(vm.$el.classList.contains('user-can-drag')).toBe(true);
}); });
...@@ -190,7 +180,7 @@ describe('Board card', () => { ...@@ -190,7 +180,7 @@ describe('Board card', () => {
triggerEvent('mousedown'); triggerEvent('mousedown');
triggerEvent('mouseup'); triggerEvent('mouseup');
expect(eventHub.$emit).toHaveBeenCalledWith('newDetailIssue', vm.issue, undefined); expect(eventHub.$emit).toHaveBeenCalledWith('newDetailIssue', vm.issue);
expect(boardsStore.detail.list).toEqual(vm.list); expect(boardsStore.detail.list).toEqual(vm.list);
}); });
...@@ -213,7 +203,7 @@ describe('Board card', () => { ...@@ -213,7 +203,7 @@ describe('Board card', () => {
triggerEvent('mousedown'); triggerEvent('mousedown');
triggerEvent('mouseup'); triggerEvent('mouseup');
expect(eventHub.$emit).toHaveBeenCalledWith('clearDetailIssue', undefined); expect(eventHub.$emit).toHaveBeenCalledWith('clearDetailIssue');
}); });
}); });
}); });
...@@ -12,7 +12,6 @@ import '~/boards/services/board_service'; ...@@ -12,7 +12,6 @@ import '~/boards/services/board_service';
import boardsStore from '~/boards/stores/boards_store'; import boardsStore from '~/boards/stores/boards_store';
import eventHub from '~/boards/eventhub'; import eventHub from '~/boards/eventhub';
import { listObj, listObjDuplicate, boardsMockInterceptor, mockBoardService } from './mock_data'; import { listObj, listObjDuplicate, boardsMockInterceptor, mockBoardService } from './mock_data';
import waitForPromises from '../../frontend/helpers/wait_for_promises';
describe('Store', () => { describe('Store', () => {
let mock; let mock;
...@@ -30,13 +29,6 @@ describe('Store', () => { ...@@ -30,13 +29,6 @@ describe('Store', () => {
}), }),
); );
spyOn(gl.boardService, 'moveMultipleIssues').and.callFake(
() =>
new Promise(resolve => {
resolve();
}),
);
Cookies.set('issue_board_welcome_hidden', 'false', { Cookies.set('issue_board_welcome_hidden', 'false', {
expires: 365 * 10, expires: 365 * 10,
path: '', path: '',
...@@ -384,128 +376,4 @@ describe('Store', () => { ...@@ -384,128 +376,4 @@ describe('Store', () => {
expect(state.currentBoard).toEqual(dummyBoard); expect(state.currentBoard).toEqual(dummyBoard);
}); });
}); });
describe('toggleMultiSelect', () => {
let basicIssueObj;
beforeAll(() => {
basicIssueObj = { id: 987654 };
});
afterEach(() => {
boardsStore.clearMultiSelect();
});
it('adds issue when not present', () => {
boardsStore.toggleMultiSelect(basicIssueObj);
const selectedIds = boardsStore.multiSelect.list.map(x => x.id);
expect(selectedIds.includes(basicIssueObj.id)).toEqual(true);
});
it('removes issue when issue is present', () => {
boardsStore.toggleMultiSelect(basicIssueObj);
let selectedIds = boardsStore.multiSelect.list.map(x => x.id);
expect(selectedIds.includes(basicIssueObj.id)).toEqual(true);
boardsStore.toggleMultiSelect(basicIssueObj);
selectedIds = boardsStore.multiSelect.list.map(x => x.id);
expect(selectedIds.includes(basicIssueObj.id)).toEqual(false);
});
});
describe('clearMultiSelect', () => {
it('clears all the multi selected issues', () => {
const issue1 = { id: 12345 };
const issue2 = { id: 12346 };
boardsStore.toggleMultiSelect(issue1);
boardsStore.toggleMultiSelect(issue2);
expect(boardsStore.multiSelect.list.length).toEqual(2);
boardsStore.clearMultiSelect();
expect(boardsStore.multiSelect.list.length).toEqual(0);
});
});
describe('moveMultipleIssuesToList', () => {
it('move issues on the new index', done => {
const listOne = boardsStore.addList(listObj);
const listTwo = boardsStore.addList(listObjDuplicate);
expect(boardsStore.state.lists.length).toBe(2);
setTimeout(() => {
expect(listOne.issues.length).toBe(1);
expect(listTwo.issues.length).toBe(1);
boardsStore.moveMultipleIssuesToList({
listFrom: listOne,
listTo: listTwo,
issues: listOne.issues,
newIndex: 0,
});
expect(listTwo.issues.length).toBe(1);
done();
}, 0);
});
});
describe('moveMultipleIssuesInList', () => {
it('moves multiple issues in list', done => {
const issueObj = {
title: 'Issue #1',
id: 12345,
iid: 2,
confidential: false,
labels: [],
assignees: [],
};
const issue1 = new ListIssue(issueObj);
const issue2 = new ListIssue({
...issueObj,
title: 'Issue #2',
id: 12346,
});
const list = boardsStore.addList(listObj);
waitForPromises()
.then(() => {
list.addIssue(issue1);
list.addIssue(issue2);
expect(list.issues.length).toBe(3);
expect(list.issues[0].id).not.toBe(issue2.id);
boardsStore.moveMultipleIssuesInList({
list,
issues: [issue1, issue2],
oldIndicies: [0],
newIndex: 1,
idArray: [1, 12345, 12346],
});
expect(list.issues[0].id).toBe(issue1.id);
expect(gl.boardService.moveMultipleIssues).toHaveBeenCalledWith({
ids: [issue1.id, issue2.id],
fromListId: null,
toListId: null,
moveBeforeId: 1,
moveAfterId: null,
});
done();
})
.catch(done.fail);
});
});
}); });
...@@ -22,7 +22,7 @@ describe AtomicInternalId do ...@@ -22,7 +22,7 @@ describe AtomicInternalId do
end end
context 'when value is set by ensure_project_iid!' do context 'when value is set by ensure_project_iid!' do
context 'with iid_always_track true' do context 'with iid_always_track false' do
before do before do
stub_feature_flags(iid_always_track: false) stub_feature_flags(iid_always_track: false)
end end
...@@ -33,6 +33,17 @@ describe AtomicInternalId do ...@@ -33,6 +33,17 @@ describe AtomicInternalId do
milestone.ensure_project_iid! milestone.ensure_project_iid!
subject subject
end end
it 'tracks the iid for the scope that is actually present' do
milestone.iid = external_iid
expect(InternalId).to receive(:track_greatest).once.with(milestone, scope_attrs, usage, external_iid, anything)
expect(InternalId).not_to receive(:generate_next)
# group scope is not present here, the milestone does not have a group
milestone.track_group_iid!
subject
end
end end
context 'with iid_always_track enabled' do context 'with iid_always_track enabled' do
......
# frozen_string_literal: true
require 'spec_helper'
describe Git::ProcessRefChangesService do
let(:project) { create(:project, :repository) }
let(:user) { project.owner }
let(:params) { { changes: git_changes } }
subject { described_class.new(project, user, params) }
shared_examples_for 'service for processing ref changes' do |push_service_class|
let(:service) { double(execute: true) }
let(:git_changes) { double(branch_changes: [], tag_changes: []) }
let(:changes) do
[
{ index: 0, oldrev: Gitlab::Git::BLANK_SHA, newrev: '789012', ref: "#{ref_prefix}/create" },
{ index: 1, oldrev: '123456', newrev: '789012', ref: "#{ref_prefix}/update" },
{ index: 2, oldrev: '123456', newrev: Gitlab::Git::BLANK_SHA, ref: "#{ref_prefix}/delete" }
]
end
before do
allow(git_changes).to receive(changes_method).and_return(changes)
end
it "calls #{push_service_class}" do
expect(push_service_class)
.to receive(:new)
.exactly(changes.count).times
.and_return(service)
subject.execute
end
context 'pipeline creation' do
context 'with valid .gitlab-ci.yml' do
before do
stub_ci_pipeline_to_return_yaml_file
allow_any_instance_of(Project)
.to receive(:commit)
.and_return(project.commit)
allow_any_instance_of(Repository)
.to receive(:branch_exists?)
.and_return(true)
end
context 'when git_push_create_all_pipelines is disabled' do
before do
stub_feature_flags(git_push_create_all_pipelines: false)
end
it 'creates pipeline for branches and tags' do
subject.execute
expect(Ci::Pipeline.pluck(:ref)).to contain_exactly('create', 'update', 'delete')
end
it "creates exactly #{described_class::PIPELINE_PROCESS_LIMIT} pipelines" do
stub_const("#{described_class}::PIPELINE_PROCESS_LIMIT", changes.count - 1)
expect { subject.execute }.to change { Ci::Pipeline.count }.by(described_class::PIPELINE_PROCESS_LIMIT)
end
end
context 'when git_push_create_all_pipelines is enabled' do
before do
stub_feature_flags(git_push_create_all_pipelines: true)
end
it 'creates all pipelines' do
expect { subject.execute }.to change { Ci::Pipeline.count }.by(changes.count)
end
end
end
context 'with invalid .gitlab-ci.yml' do
before do
stub_ci_pipeline_yaml_file(nil)
end
it 'does not create a pipeline' do
expect { subject.execute }.not_to change { Ci::Pipeline.count }
end
end
end
end
context 'branch changes' do
let(:changes_method) { :branch_changes }
let(:ref_prefix) { 'refs/heads' }
it_behaves_like 'service for processing ref changes', Git::BranchPushService
end
context 'tag changes' do
let(:changes_method) { :tag_changes }
let(:ref_prefix) { 'refs/tags' }
it_behaves_like 'service for processing ref changes', Git::TagPushService
end
end
...@@ -6,7 +6,8 @@ describe Issues::UpdateService, :mailer do ...@@ -6,7 +6,8 @@ describe Issues::UpdateService, :mailer do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:user2) { create(:user) } let(:user2) { create(:user) }
let(:user3) { create(:user) } let(:user3) { create(:user) }
let(:project) { create(:project) } let(:group) { create(:group, :public) }
let(:project) { create(:project, :repository, group: group) }
let(:label) { create(:label, project: project) } let(:label) { create(:label, project: project) }
let(:label2) { create(:label) } let(:label2) { create(:label) }
...@@ -667,6 +668,7 @@ describe Issues::UpdateService, :mailer do ...@@ -667,6 +668,7 @@ describe Issues::UpdateService, :mailer do
context 'updating mentions' do context 'updating mentions' do
let(:mentionable) { issue } let(:mentionable) { issue }
include_examples 'updating mentions', described_class include_examples 'updating mentions', described_class
end end
......
...@@ -5,7 +5,8 @@ require 'spec_helper' ...@@ -5,7 +5,8 @@ require 'spec_helper'
describe MergeRequests::UpdateService, :mailer do describe MergeRequests::UpdateService, :mailer do
include ProjectForksHelper include ProjectForksHelper
let(:project) { create(:project, :repository) } let(:group) { create(:group, :public) }
let(:project) { create(:project, :repository, group: group) }
let(:user) { create(:user) } let(:user) { create(:user) }
let(:user2) { create(:user) } let(:user2) { create(:user) }
let(:user3) { create(:user) } let(:user3) { create(:user) }
...@@ -472,6 +473,7 @@ describe MergeRequests::UpdateService, :mailer do ...@@ -472,6 +473,7 @@ describe MergeRequests::UpdateService, :mailer do
context 'updating mentions' do context 'updating mentions' do
let(:mentionable) { merge_request } let(:mentionable) { merge_request }
include_examples 'updating mentions', described_class include_examples 'updating mentions', described_class
end end
......
...@@ -3,17 +3,25 @@ ...@@ -3,17 +3,25 @@
require 'spec_helper' require 'spec_helper'
describe Notes::UpdateService do describe Notes::UpdateService do
let(:project) { create(:project) } let(:group) { create(:group, :public) }
let(:project) { create(:project, :public, group: group) }
let(:private_group) { create(:group, :private) }
let(:private_project) { create(:project, :private, group: private_group) }
let(:user) { create(:user) } let(:user) { create(:user) }
let(:user2) { create(:user) } let(:user2) { create(:user) }
let(:user3) { create(:user) } let(:user3) { create(:user) }
let(:issue) { create(:issue, project: project) } let(:issue) { create(:issue, project: project) }
let(:issue2) { create(:issue, project: private_project) }
let(:note) { create(:note, project: project, noteable: issue, author: user, note: "Old note #{user2.to_reference}") } let(:note) { create(:note, project: project, noteable: issue, author: user, note: "Old note #{user2.to_reference}") }
before do before do
project.add_maintainer(user) project.add_maintainer(user)
project.add_developer(user2) project.add_developer(user2)
project.add_developer(user3) project.add_developer(user3)
group.add_developer(user3)
private_group.add_developer(user)
private_group.add_developer(user2)
private_project.add_developer(user3)
end end
describe '#execute' do describe '#execute' do
...@@ -46,13 +54,17 @@ describe Notes::UpdateService do ...@@ -46,13 +54,17 @@ describe Notes::UpdateService do
end end
context 'todos' do context 'todos' do
let!(:todo) { create(:todo, :assigned, user: user, project: project, target: issue, author: user2) } shared_examples 'does not update todos' do
it 'keep todos' do
expect(todo.reload).to be_pending
end
context 'when the note change' do it 'does not create any new todos' do
before do expect(Todo.count).to eq(1)
update_note({ note: "New note #{user2.to_reference} #{user3.to_reference}" })
end end
end
shared_examples 'creates one todo' do
it 'marks todos as done' do it 'marks todos as done' do
expect(todo.reload).to be_done expect(todo.reload).to be_done
end end
...@@ -62,17 +74,75 @@ describe Notes::UpdateService do ...@@ -62,17 +74,75 @@ describe Notes::UpdateService do
end end
end end
context 'when the note does not change' do context 'when note includes a user mention' do
before do let!(:todo) { create(:todo, :assigned, user: user, project: project, target: issue, author: user2) }
update_note({ note: "Old note #{user2.to_reference}" })
context 'when the note does not change mentions' do
before do
update_note({ note: "Old note #{user2.to_reference}" })
end
it_behaves_like 'does not update todos'
end end
it 'keep todos' do context 'when the note changes to include one more user mention' do
expect(todo.reload).to be_pending before do
update_note({ note: "New note #{user2.to_reference} #{user3.to_reference}" })
end
it_behaves_like 'creates one todo'
end end
it 'does not create any new todos' do context 'when the note changes to include a group mentions' do
expect(Todo.count).to eq(1) before do
update_note({ note: "New note #{private_group.to_reference}" })
end
it_behaves_like 'creates one todo'
end
end
context 'when note includes a group mention' do
context 'when the group is public' do
let(:note) { create(:note, project: project, noteable: issue, author: user, note: "Old note #{group.to_reference}") }
let!(:todo) { create(:todo, :assigned, user: user, project: project, target: issue, author: user2) }
context 'when the note does not change mentions' do
before do
update_note({ note: "Old note #{group.to_reference}" })
end
it_behaves_like 'does not update todos'
end
context 'when the note changes mentions' do
before do
update_note({ note: "New note #{user2.to_reference} #{user3.to_reference}" })
end
it_behaves_like 'creates one todo'
end
end
context 'when the group is private' do
let(:note) { create(:note, project: project, noteable: issue, author: user, note: "Old note #{private_group.to_reference}") }
let!(:todo) { create(:todo, :assigned, user: user, project: project, target: issue, author: user2) }
context 'when the note does not change mentions' do
before do
update_note({ note: "Old note #{private_group.to_reference}" })
end
it_behaves_like 'does not update todos'
end
context 'when the note changes mentions' do
before do
update_note({ note: "New note #{user2.to_reference} #{user3.to_reference}" })
end
it_behaves_like 'creates one todo'
end
end end
end end
end end
......
...@@ -47,6 +47,10 @@ shared_examples_for 'AtomicInternalId' do |validate_presence: true| ...@@ -47,6 +47,10 @@ shared_examples_for 'AtomicInternalId' do |validate_presence: true|
end end
describe 'internal id generation' do describe 'internal id generation' do
before do
stub_feature_flags(iid_always_track: false)
end
subject { instance.save! } subject { instance.save! }
it 'calls InternalId.generate_next and sets internal id attribute' do it 'calls InternalId.generate_next and sets internal id attribute' do
......
# frozen_string_literal: true # frozen_string_literal: true
RSpec.shared_examples 'updating mentions' do |service_class| RSpec.shared_examples 'updating mentions' do |service_class|
let(:mentioned_user) { create(:user) } let(:service_class) { service_class }
let(:service_class) { service_class } let(:mentioned_user) { create(:user) }
let(:group_member1) { create(:user) }
let(:group_member2) { create(:user) }
let(:external_group) { create(:group, :private) }
before do before do
project.add_developer(mentioned_user) project.add_developer(mentioned_user)
group.add_developer(group_member1)
group.add_developer(group_member2)
end end
def update_mentionable(opts) def update_mentionable(opts)
...@@ -16,23 +21,74 @@ RSpec.shared_examples 'updating mentions' do |service_class| ...@@ -16,23 +21,74 @@ RSpec.shared_examples 'updating mentions' do |service_class|
mentionable.reload mentionable.reload
end end
context 'in title' do context 'when mentioning a different user' do
before do context 'in title' do
update_mentionable(title: mentioned_user.to_reference) before do
update_mentionable(title: "For #{mentioned_user.to_reference}")
end
it 'emails only the newly-mentioned user' do
should_only_email(mentioned_user)
end
end
context 'in description' do
before do
update_mentionable(description: "For #{mentioned_user.to_reference}")
end
it 'emails only the newly-mentioned user' do
should_only_email(mentioned_user)
end
end
end
context 'when mentioning a user and a group with access to' do
shared_examples 'updating attribute with allowed mentions' do |attribute|
before do
update_mentionable(
{ attribute => "For #{group.to_reference}, cc: #{mentioned_user.to_reference}" }
)
end
it 'emails group members' do
should_email(mentioned_user)
should_email(group_member1)
should_email(group_member2)
end
end
context 'when group is public' do
it_behaves_like 'updating attribute with allowed mentions', :title
it_behaves_like 'updating attribute with allowed mentions', :description
end end
it 'emails only the newly-mentioned user' do context 'when the group is private' do
should_only_email(mentioned_user) before do
group.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
end
it_behaves_like 'updating attribute with allowed mentions', :title
it_behaves_like 'updating attribute with allowed mentions', :description
end end
end end
context 'in description' do context 'when mentioning a user and a group without access to' do
before do shared_examples 'updating attribute with not allowed mentions' do |attribute|
update_mentionable(description: mentioned_user.to_reference) before do
update_mentionable(
{ attribute => "For #{external_group.to_reference}, cc: #{mentioned_user.to_reference}" }
)
end
it 'emails mentioned user' do
should_only_email(mentioned_user)
end
end end
it 'emails only the newly-mentioned user' do context 'when the group is private' do
should_only_email(mentioned_user) it_behaves_like 'updating attribute with not allowed mentions', :title
it_behaves_like 'updating attribute with not allowed mentions', :description
end end
end end
end end
...@@ -73,8 +73,7 @@ describe PostReceive do ...@@ -73,8 +73,7 @@ describe PostReceive do
context 'empty changes' do context 'empty changes' do
it "does not call any PushService but runs after project hooks" do it "does not call any PushService but runs after project hooks" do
expect(Git::BranchPushService).not_to receive(:new) expect(Git::ProcessRefChangesService).not_to receive(:new)
expect(Git::TagPushService).not_to receive(:new)
expect_next_instance_of(SystemHooksService) { |service| expect(service).to receive(:execute_hooks) } expect_next_instance_of(SystemHooksService) { |service| expect(service).to receive(:execute_hooks) }
perform(changes: "") perform(changes: "")
...@@ -87,8 +86,7 @@ describe PostReceive do ...@@ -87,8 +86,7 @@ describe PostReceive do
let!(:key_id) { "" } let!(:key_id) { "" }
it 'returns false' do it 'returns false' do
expect(Git::BranchPushService).not_to receive(:new) expect(Git::ProcessRefChangesService).not_to receive(:new)
expect(Git::TagPushService).not_to receive(:new)
expect(perform).to be false expect(perform).to be false
end end
...@@ -131,13 +129,11 @@ describe PostReceive do ...@@ -131,13 +129,11 @@ describe PostReceive do
perform perform
end end
it 'calls Git::BranchPushService' do it 'calls Git::ProcessRefChangesService' do
expect_any_instance_of(Git::BranchPushService) do |service| expect_next_instance_of(Git::ProcessRefChangesService) do |service|
expect(service).to receive(:execute).and_return(true) expect(service).to receive(:execute).and_return(true)
end end
expect(Git::TagPushService).not_to receive(:new)
perform perform
end end
...@@ -174,8 +170,6 @@ describe PostReceive do ...@@ -174,8 +170,6 @@ describe PostReceive do
654321 210987 refs/tags/tag1 654321 210987 refs/tags/tag1
654322 210986 refs/tags/tag2 654322 210986 refs/tags/tag2
654323 210985 refs/tags/tag3 654323 210985 refs/tags/tag3
654324 210984 refs/tags/tag4
654325 210983 refs/tags/tag5
EOF EOF
end end
...@@ -189,23 +183,19 @@ describe PostReceive do ...@@ -189,23 +183,19 @@ describe PostReceive do
perform perform
end end
it "only invalidates tags once" do it 'only invalidates tags once' do
expect(project.repository).to receive(:repository_event).exactly(5).times.with(:push_tag).and_call_original expect(project.repository).to receive(:repository_event).exactly(3).times.with(:push_tag).and_call_original
expect(project.repository).to receive(:expire_caches_for_tags).once.and_call_original expect(project.repository).to receive(:expire_caches_for_tags).once.and_call_original
expect(project.repository).to receive(:expire_tags_cache).once.and_call_original expect(project.repository).to receive(:expire_tags_cache).once.and_call_original
perform perform
end end
it "calls Git::TagPushService" do it 'calls Git::ProcessRefChangesService' do
expect(Git::BranchPushService).not_to receive(:new) expect_next_instance_of(Git::ProcessRefChangesService) do |service|
expect_any_instance_of(Git::TagPushService) do |service|
expect(service).to receive(:execute).and_return(true) expect(service).to receive(:execute).and_return(true)
end end
expect(Git::BranchPushService).not_to receive(:new)
perform perform
end end
...@@ -223,8 +213,7 @@ describe PostReceive do ...@@ -223,8 +213,7 @@ describe PostReceive do
let(:changes) { "123456 789012 refs/merge-requests/123" } let(:changes) { "123456 789012 refs/merge-requests/123" }
it "does not call any of the services" do it "does not call any of the services" do
expect(Git::BranchPushService).not_to receive(:new) expect(Git::ProcessRefChangesService).not_to receive(:new)
expect(Git::TagPushService).not_to receive(:new)
perform perform
end end
...@@ -232,72 +221,6 @@ describe PostReceive do ...@@ -232,72 +221,6 @@ describe PostReceive do
it_behaves_like 'not updating remote mirrors' it_behaves_like 'not updating remote mirrors'
end end
context "gitlab-ci.yml" do
let(:changes) do
<<-EOF.strip_heredoc
123456 789012 refs/heads/feature
654321 210987 refs/tags/tag
123456 789012 refs/heads/feature2
123458 789013 refs/heads/feature3
123459 789015 refs/heads/feature4
EOF
end
let(:changes_count) { changes.lines.count }
subject { perform }
context "with valid .gitlab-ci.yml" do
before do
stub_ci_pipeline_to_return_yaml_file
allow_any_instance_of(Project)
.to receive(:commit)
.and_return(project.commit)
allow_any_instance_of(Repository)
.to receive(:branch_exists?)
.and_return(true)
end
context 'when git_push_create_all_pipelines is disabled' do
before do
stub_feature_flags(git_push_create_all_pipelines: false)
end
it "creates pipeline for branches and tags" do
subject
expect(Ci::Pipeline.pluck(:ref)).to contain_exactly("feature", "tag", "feature2", "feature3")
end
it "creates exactly #{described_class::PIPELINE_PROCESS_LIMIT} pipelines" do
expect(changes_count).to be > described_class::PIPELINE_PROCESS_LIMIT
expect { subject }.to change { Ci::Pipeline.count }.by(described_class::PIPELINE_PROCESS_LIMIT)
end
end
context 'when git_push_create_all_pipelines is enabled' do
before do
stub_feature_flags(git_push_create_all_pipelines: true)
end
it "creates all pipelines" do
expect { subject }.to change { Ci::Pipeline.count }.by(changes_count)
end
end
end
context "does not create a Ci::Pipeline" do
before do
stub_ci_pipeline_yaml_file(nil)
end
it { expect { subject }.not_to change { Ci::Pipeline.count } }
end
end
context 'after project changes hooks' do context 'after project changes hooks' do
let(:changes) { '123456 789012 refs/heads/tést' } let(:changes) { '123456 789012 refs/heads/tést' }
let(:fake_hook_data) { Hash.new(event_name: 'repository_update') } let(:fake_hook_data) { Hash.new(event_name: 'repository_update') }
...@@ -306,7 +229,7 @@ describe PostReceive do ...@@ -306,7 +229,7 @@ describe PostReceive do
allow_any_instance_of(Gitlab::DataBuilder::Repository).to receive(:update).and_return(fake_hook_data) allow_any_instance_of(Gitlab::DataBuilder::Repository).to receive(:update).and_return(fake_hook_data)
# silence hooks so we can isolate # silence hooks so we can isolate
allow_any_instance_of(Key).to receive(:post_create_hook).and_return(true) allow_any_instance_of(Key).to receive(:post_create_hook).and_return(true)
expect_next_instance_of(Git::BranchPushService) do |service| expect_next_instance_of(Git::ProcessRefChangesService) do |service|
expect(service).to receive(:execute).and_return(true) expect(service).to receive(:execute).and_return(true)
end end
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment