Commit d2798d60 authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent d8211a0e
/* eslint-disable class-methods-use-this, no-unused-vars */
/* eslint-disable class-methods-use-this */
import $ from 'jquery';
......@@ -61,7 +61,7 @@ export default class TemplateSelector {
return this.requestFile(item);
}
requestFile(item) {
requestFile() {
// This `requestFile` method is an abstract method that should
// be added by all subclasses.
}
......
/* eslint-disable func-names, no-var, prefer-arrow-callback, no-else-return, consistent-return, prefer-template, one-var, no-unused-vars, no-return-assign, no-unused-expressions, no-sequences */
/* eslint-disable func-names, no-var, prefer-arrow-callback, no-else-return, consistent-return, prefer-template, one-var, no-return-assign, no-unused-expressions, no-sequences */
import $ from 'jquery';
......@@ -12,11 +12,8 @@ export default class ImageFile {
this.requestImageInfo(
$('.two-up.view .frame.deleted img', this.file),
(function(_this) {
return function(deletedWidth, deletedHeight) {
return _this.requestImageInfo($('.two-up.view .frame.added img', _this.file), function(
width,
height,
) {
return function() {
return _this.requestImageInfo($('.two-up.view .frame.added img', _this.file), function() {
_this.initViewModes();
// Load two-up view after images are loaded
......@@ -112,7 +109,7 @@ export default class ImageFile {
maxHeight = 0;
$('.frame', view)
.each(
(function(_this) {
(function() {
return function(index, frame) {
var height, width;
width = $(frame).width();
......@@ -196,13 +193,7 @@ export default class ImageFile {
return $('.onion-skin.view', this.file).each(
(function(_this) {
return function(index, view) {
var $frame,
$track,
$dragger,
$frameAdded,
framePadding,
ref,
dragging = false;
var $frame, $track, $dragger, $frameAdded, framePadding, ref;
(ref = _this.prepareFrames(view)), ([maxWidth, maxHeight] = ref);
$frame = $('.onion-skin-frame', view);
$frameAdded = $('.frame.added', view);
......
/* eslint-disable func-names, no-underscore-dangle, no-var, one-var, vars-on-top, no-unused-vars, no-shadow, no-cond-assign, prefer-arrow-callback, no-return-assign, no-else-return, camelcase, no-lonely-if, guard-for-in, no-restricted-syntax, consistent-return, prefer-template, no-param-reassign, no-loop-func */
/* global fuzzaldrinPlus */
/* eslint-disable func-names, no-underscore-dangle, no-var, one-var, vars-on-top, no-shadow, no-cond-assign, prefer-arrow-callback, no-return-assign, no-else-return, camelcase, no-lonely-if, guard-for-in, no-restricted-syntax, consistent-return, prefer-template, no-param-reassign, no-loop-func */
import $ from 'jquery';
import _ from 'underscore';
......@@ -66,12 +65,10 @@ GitLabDropdownInput = (function() {
})();
GitLabDropdownFilter = (function() {
var ARROW_KEY_CODES, BLUR_KEYCODES, HAS_VALUE_CLASS;
var BLUR_KEYCODES, HAS_VALUE_CLASS;
BLUR_KEYCODES = [27, 40];
ARROW_KEY_CODES = [38, 40];
HAS_VALUE_CLASS = 'has-value';
function GitLabDropdownFilter(input, options) {
......@@ -877,9 +874,8 @@ GitLabDropdown = (function() {
};
GitLabDropdown.prototype.addArrowKeyEvent = function() {
var $input, ARROW_KEY_CODES, selector;
var ARROW_KEY_CODES, selector;
ARROW_KEY_CODES = [38, 40];
$input = this.dropdown.find('.dropdown-input-field');
selector = SELECTABLE_CLASSES;
if (this.dropdown.find('.dropdown-toggle-page').length) {
selector = '.dropdown-page-one ' + selector;
......
/* eslint-disable no-var, one-var, no-unused-vars, consistent-return */
/* eslint-disable no-var, one-var, consistent-return */
import $ from 'jquery';
import axios from './lib/utils/axios_utils';
import { addDelimiter } from './lib/utils/text_utility';
import flash from './flash';
import TaskList from './task_list';
import CreateMergeRequestDropdown from './create_merge_request_dropdown';
import IssuablesHelper from './helpers/issuables_helper';
import { __ } from './locale';
......
/* eslint-disable class-methods-use-this, no-underscore-dangle, no-param-reassign, no-unused-vars, func-names */
/* eslint-disable class-methods-use-this, no-underscore-dangle, no-param-reassign, func-names */
import $ from 'jquery';
import Sortable from 'sortablejs';
......@@ -50,7 +50,7 @@ export default class LabelManager {
$(e.currentTarget).tooltip('hide');
}
toggleEmptyState($label, $btn, action) {
toggleEmptyState() {
this.emptyState.classList.toggle(
'hidden',
Boolean(this.prioritizedLabels[0].querySelector(':scope > li')),
......@@ -61,7 +61,6 @@ export default class LabelManager {
if (persistState == null) {
persistState = true;
}
const _this = this;
const url = $label.find('.js-toggle-priority').data('url');
let $target = this.prioritizedLabels;
let $from = this.otherLabels;
......
/* eslint-disable no-useless-return, func-names, no-var, no-underscore-dangle, prefer-arrow-callback, one-var, no-unused-vars, prefer-template, no-new, consistent-return, object-shorthand, no-shadow, no-param-reassign, vars-on-top, no-lonely-if, no-else-return, dot-notation, no-empty */
/* eslint-disable no-useless-return, func-names, no-var, no-underscore-dangle, prefer-arrow-callback, one-var, prefer-template, no-new, consistent-return, object-shorthand, no-shadow, no-param-reassign, vars-on-top, no-lonely-if, no-else-return, dot-notation, no-empty */
/* global Issuable */
/* global ListLabel */
......@@ -26,7 +26,6 @@ export default class LabelsSelect {
$els.each(function(i, dropdown) {
var $block,
$colorPreview,
$dropdown,
$form,
$loading,
......@@ -35,8 +34,6 @@ export default class LabelsSelect {
$value,
abilityName,
defaultLabel,
enableLabelCreateButton,
issueURLSplit,
issueUpdateURL,
labelUrl,
namespacePath,
......@@ -47,16 +44,11 @@ export default class LabelsSelect {
showNo,
$sidebarLabelTooltip,
initialSelected,
$toggleText,
fieldName,
useId,
propertyName,
showMenuAbove,
$container,
$dropdownContainer;
$dropdown = $(dropdown);
$dropdownContainer = $dropdown.closest('.labels-filter');
$toggleText = $dropdown.find('.dropdown-toggle-text');
namespacePath = $dropdown.data('namespacePath');
projectPath = $dropdown.data('projectPath');
issueUpdateURL = $dropdown.data('issueUpdate');
......@@ -77,10 +69,6 @@ export default class LabelsSelect {
$value = $block.find('.value');
$loading = $block.find('.block-loading').fadeOut();
fieldName = $dropdown.data('fieldName');
useId = $dropdown.is(
'.js-issuable-form-dropdown, .js-filter-bulk-update, .js-label-sidebar-dropdown',
);
propertyName = useId ? 'id' : 'title';
initialSelected = $selectbox
.find('input[name="' + $dropdown.data('fieldName') + '"]')
.map(function() {
......@@ -124,7 +112,7 @@ export default class LabelsSelect {
axios
.put(issueUpdateURL, data)
.then(({ data }) => {
var labelCount, template, labelTooltipTitle, labelTitles, formattedLabels;
var labelCount, template, labelTooltipTitle, labelTitles;
$loading.fadeOut();
$dropdown.trigger('loaded.gl.dropdown');
$selectbox.hide();
......@@ -246,12 +234,10 @@ export default class LabelsSelect {
renderRow: function(label) {
var linkEl,
listItemEl,
color,
colorEl,
indeterminate,
removesAll,
selectedClass,
spacing,
i,
marked,
dropdownValue;
......@@ -378,7 +364,7 @@ export default class LabelsSelect {
}
},
hidden: function() {
var isIssueIndex, isMRIndex, page, selectedLabels;
var isIssueIndex, isMRIndex, page;
page = $('body').attr('data-page');
isIssueIndex = page === 'projects:issues:index';
isMRIndex = page === 'projects:merge_requests:index';
......@@ -395,9 +381,6 @@ export default class LabelsSelect {
}
if ($dropdown.hasClass('js-multiselect')) {
if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
selectedLabels = $dropdown
.closest('form')
.find("input:hidden[name='" + $dropdown.data('fieldName') + "']");
Issuable.filterResults($dropdown.closest('form'));
} else if ($dropdown.hasClass('js-filter-submit')) {
$dropdown.closest('form').submit();
......@@ -495,7 +478,7 @@ export default class LabelsSelect {
}
}
},
opened: function(e) {
opened: function() {
if ($dropdown.hasClass('js-issue-board-sidebar')) {
const previousSelection = $dropdown.attr('data-selected');
this.selected = previousSelection ? previousSelection.split(',') : [];
......
/* eslint-disable func-names, no-var, no-param-reassign, one-var, operator-assignment, no-else-return, prefer-template, prefer-arrow-callback, consistent-return, no-unused-vars */
/* eslint-disable func-names, no-var, no-param-reassign, one-var, operator-assignment, no-else-return, prefer-template, prefer-arrow-callback, consistent-return */
import $ from 'jquery';
import { insertText } from '~/lib/utils/common_utils';
......@@ -157,7 +157,7 @@ export function insertMarkdownText({
if (tag === LINK_TAG_PATTERN) {
if (URL) {
try {
const ignoredUrl = new URL(selected);
new URL(selected); // eslint-disable-line no-new
// valid url
tag = '[text]({text})';
select = 'text';
......
/* eslint-disable one-var, no-unused-vars, object-shorthand, no-else-return, no-self-compare, consistent-return, no-param-reassign, no-shadow */
/* eslint-disable one-var, object-shorthand, no-else-return, no-self-compare, consistent-return, no-param-reassign, no-shadow */
/* global Issuable */
/* global ListMilestone */
......@@ -37,7 +37,6 @@ export default class MilestoneSelect {
selectedMilestone,
selectedMilestoneDefault;
const $dropdown = $(dropdown);
const projectId = $dropdown.data('projectId');
const milestonesUrl = $dropdown.data('milestones');
const issueUpdateURL = $dropdown.data('issueUpdate');
const showNo = $dropdown.data('showNo');
......@@ -48,7 +47,6 @@ export default class MilestoneSelect {
const useId = $dropdown.data('useId');
const defaultLabel = $dropdown.data('defaultLabel');
const defaultNo = $dropdown.data('defaultNo');
const issuableId = $dropdown.data('issuableId');
const abilityName = $dropdown.data('abilityName');
const $selectBox = $dropdown.closest('.selectbox');
const $block = $selectBox.closest('.block');
......@@ -121,7 +119,7 @@ export default class MilestoneSelect {
fields: ['title'],
},
selectable: true,
toggleLabel: (selected, el, e) => {
toggleLabel: (selected, el) => {
if (selected && 'id' in selected && $(el).hasClass('is-active')) {
return selected.title;
} else {
......@@ -153,7 +151,7 @@ export default class MilestoneSelect {
},
vue: $dropdown.hasClass('js-issue-board-sidebar'),
clicked: clickEvent => {
const { $el, e } = clickEvent;
const { e } = clickEvent;
let selected = clickEvent.selectedObj;
let data, modalStoreFilter;
......
/* eslint-disable func-names, no-var, one-var, no-loop-func, consistent-return, no-unused-vars, prefer-template, prefer-arrow-callback, camelcase */
/* eslint-disable func-names, no-var, one-var, no-loop-func, consistent-return, prefer-template, prefer-arrow-callback, camelcase */
import $ from 'jquery';
import { __ } from '../locale';
import axios from '../lib/utils/axios_utils';
import flash from '../flash';
import Raphael from './raphael';
export default (function() {
......@@ -104,7 +103,7 @@ export default (function() {
};
BranchGraph.prototype.buildGraph = function() {
var cuday, cumonth, day, j, len, mm, ref;
var cuday, cumonth, day, len, mm, ref;
const { r } = this;
cuday = 0;
cumonth = '';
......@@ -178,7 +177,7 @@ export default (function() {
return $(element).scroll(
(function(_this) {
return function(event) {
return function() {
return _this.renderPartialGraph();
};
})(this),
......@@ -214,7 +213,7 @@ export default (function() {
};
BranchGraph.prototype.appendLabel = function(x, y, commit) {
var label, rect, shortrefs, text, textbox, triangle;
var label, rect, shortrefs, text, textbox;
if (!commit.refs) {
return;
......@@ -239,7 +238,8 @@ export default (function() {
'fill-opacity': 0.5,
stroke: 'none',
});
triangle = r.path(['M', x - 5, y, 'L', x - 15, y - 4, 'L', x - 15, y + 4, 'Z']).attr({
// Generate the triangle right of the tag box
r.path(['M', x - 5, y, 'L', x - 15, y - 4, 'L', x - 15, y + 4, 'Z']).attr({
fill: '#000',
'fill-opacity': 0.5,
stroke: 'none',
......
......@@ -2,10 +2,9 @@
no-unused-expressions, one-var, default-case,
prefer-template, consistent-return, no-alert, no-return-assign,
no-param-reassign, prefer-arrow-callback, no-else-return, vars-on-top,
no-unused-vars, no-shadow, no-useless-escape, class-methods-use-this */
no-shadow, no-useless-escape, class-methods-use-this */
/* global ResolveService */
/* global mrRefreshWidgetUrl */
/*
old_notes_spec.js is the spec for the legacy, jQuery notes application. It has nothing to do with the new, fancy Vue notes app.
......@@ -37,7 +36,6 @@ import {
isMetaKey,
isInMRPage,
} from './lib/utils/common_utils';
import imageDiffHelper from './image_diff/helpers/index';
import { localTimeAgo } from './lib/utils/datetime_utility';
import { sprintf, s__, __ } from './locale';
......@@ -683,7 +681,7 @@ export default class Notes {
);
}
updateNoteError($parentTimeline) {
updateNoteError() {
// eslint-disable-next-line no-new
new Flash(
__('Your comment could not be updated! Please check your network connection and try again.'),
......@@ -697,7 +695,6 @@ export default class Notes {
*/
addDiscussionNote($form, note, isNewDiffComment) {
if ($form.attr('data-resolve-all') != null) {
var projectPath = $form.data('projectPath');
var discussionId = $form.data('discussionId');
var mergeRequestId = $form.data('noteableIid');
......@@ -746,7 +743,6 @@ export default class Notes {
if (currentContent === initialContent) {
this.removeNoteEditForm($el);
} else {
var $buttons = $el.find('.note-form-actions');
var isWidgetVisible = isInViewport($el.get(0));
if (!isWidgetVisible) {
......@@ -766,7 +762,7 @@ export default class Notes {
* Replaces the note text with the note edit form
* Adds a data attribute to the form with the original content of the note for cancellations
*/
showEditForm(e, scrollTo, myLastNote) {
showEditForm(e) {
e.preventDefault();
var $target = $(e.target);
......@@ -850,16 +846,11 @@ export default class Notes {
* Removes the whole discussion if the last note is being removed.
*/
removeNote(e) {
var noteElId, noteId, dataNoteId, $note, lineHolder;
var noteElId, $note;
$note = $(e.currentTarget).closest('.note');
noteElId = $note.attr('id');
noteId = $note.attr('data-note-id');
lineHolder = $(e.currentTarget)
.closest('.notes[data-discussion-id]')
.closest('.notes_holder')
.prev('.line_holder');
$(`.note[id="${noteElId}"]`).each(
(function(_this) {
(function() {
// A same note appears in the "Discussion" and in the "Changes" tab, we have
// to remove all. Using $('.note[id='noteId']') ensure we get all the notes,
// where $('#noteId') would return only one.
......@@ -1064,25 +1055,8 @@ export default class Notes {
this.setupDiscussionNoteForm($link, newForm);
}
toggleDiffNote({
target,
lineType,
forceShow,
showReplyInput = false,
currentUsername,
currentUserAvatar,
currentUserFullname,
}) {
var $link,
addForm,
hasNotes,
newForm,
noteForm,
replyButton,
row,
rowCssToAdd,
targetContent,
isDiffCommentAvatar;
toggleDiffNote({ target, lineType, forceShow, showReplyInput = false }) {
var $link, addForm, hasNotes, newForm, noteForm, replyButton, row, rowCssToAdd;
$link = $(target);
row = $link.closest('tr');
const nextRow = row.next();
......@@ -1515,7 +1489,7 @@ export default class Notes {
let tempFormContent;
// Identify executed quick actions from `formContent`
const executedCommands = availableQuickActions.filter((command, index) => {
const executedCommands = availableQuickActions.filter(command => {
const commandRegex = new RegExp(`/${command.name}`);
return commandRegex.test(formContent);
});
......@@ -1840,8 +1814,6 @@ export default class Notes {
const $noteBody = $editingNote.find('.js-task-list-container');
const $noteBodyText = $noteBody.find('.note-text');
const { formData, formContent, formAction } = this.getFormData($form);
const $diffFile = $form.closest('.diff-file');
const $notesContainer = $form.closest('.notes');
// Cache original comment content
const cachedNoteBodyText = $noteBodyText.html();
......
/* eslint-disable func-names, object-shorthand, no-var, one-var, camelcase, no-param-reassign, no-return-assign, prefer-arrow-callback, consistent-return, no-unused-vars, no-cond-assign, no-else-return */
/* eslint-disable func-names, object-shorthand, no-var, one-var, camelcase, no-param-reassign, no-return-assign, prefer-arrow-callback, consistent-return, no-cond-assign, no-else-return */
import _ from 'underscore';
export default {
......@@ -126,7 +126,7 @@ export default {
_.each(
_.omit(log_entry, 'author_name', 'author_email'),
(function(_this) {
return function(value, key) {
return function(value) {
if (_this.in_range(value.date, date_range)) {
parsed_entry.dates[value.date] = value[field];
parsed_entry.commits += value.commits;
......
/* eslint-disable no-useless-escape, no-var, no-underscore-dangle, func-names, no-unused-vars, no-return-assign, object-shorthand, one-var, consistent-return, class-methods-use-this */
/* eslint-disable no-useless-escape, no-var, no-underscore-dangle, func-names, no-return-assign, object-shorthand, one-var, consistent-return, class-methods-use-this */
import $ from 'jquery';
import 'cropper';
import _ from 'underscore';
(global => {
(() => {
// Matches everything but the file name
const FILENAMEREGEX = /^.*[\\\/]/;
......@@ -69,7 +69,7 @@ import _ from 'underscore';
this.modalCrop.on('shown.bs.modal', this.onModalShow);
this.modalCrop.on('hidden.bs.modal', this.onModalHide);
this.uploadImageBtn.on('click', this.onUploadImageBtnClick);
this.cropActionsBtn.on('click', function(e) {
this.cropActionsBtn.on('click', function() {
var btn;
btn = this;
return _this.onActionBtnClick(btn);
......@@ -128,10 +128,10 @@ import _ from 'underscore';
}
onActionBtnClick(btn) {
var data, result;
var data;
data = $(btn).data();
if (this.modalCropImg.data('cropper') && data.method) {
return (result = this.modalCropImg.cropper(data.method, data.option));
return this.modalCropImg.cropper(data.method, data.option);
}
}
......@@ -151,12 +151,11 @@ import _ from 'underscore';
}
dataURLtoBlob(dataURL) {
var array, binary, i, len, v;
var array, binary, i, len;
binary = atob(dataURL.split(',')[1]);
array = [];
for (i = 0, len = binary.length; i < len; i += 1) {
v = binary[i];
array.push(binary.charCodeAt(i));
}
return new Blob([new Uint8Array(array)], {
......
/* eslint-disable func-names, no-var, consistent-return, one-var, no-cond-assign, prefer-template, no-unused-vars, no-return-assign */
/* eslint-disable func-names, no-var, consistent-return, one-var, no-cond-assign, prefer-template, no-return-assign */
import $ from 'jquery';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
......@@ -8,9 +8,8 @@ import { __ } from '~/locale';
// highlight text(awefwbwgtc -> <b>a</b>wefw<b>b</b>wgt<b>c</b> )
const highlighter = function(element, text, matches) {
var highlightText, j, lastIndex, len, matchIndex, matchedChars, unmatched;
var j, lastIndex, len, matchIndex, matchedChars, unmatched;
lastIndex = 0;
highlightText = '';
matchedChars = [];
for (j = 0, len = matches.length; j < len; j += 1) {
matchIndex = matches[j];
......
/* eslint-disable func-names, no-var, no-unused-vars, consistent-return, one-var, prefer-template, no-else-return, no-param-reassign */
/* eslint-disable func-names, no-var, consistent-return, one-var, prefer-template, no-else-return, no-param-reassign */
import $ from 'jquery';
import _ from 'underscore';
......@@ -7,7 +7,7 @@ import flash from './flash';
import axios from './lib/utils/axios_utils';
import { sprintf, s__, __ } from './locale';
function Sidebar(currentUser) {
function Sidebar() {
this.toggleTodo = this.toggleTodo.bind(this);
this.sidebar = $('aside');
......@@ -15,9 +15,9 @@ function Sidebar(currentUser) {
this.addEventListeners();
}
Sidebar.initialize = function(currentUser) {
Sidebar.initialize = function() {
if (!this.instance) {
this.instance = new Sidebar(currentUser);
this.instance = new Sidebar();
}
};
......@@ -77,7 +77,7 @@ Sidebar.prototype.sidebarToggleClicked = function(e, triggered) {
};
Sidebar.prototype.toggleTodo = function(e) {
var $btnText, $this, $todoLoading, ajaxType, url;
var $this, ajaxType, url;
$this = $(e.currentTarget);
ajaxType = $this.data('deletePath') ? 'delete' : 'post';
......@@ -140,7 +140,7 @@ Sidebar.prototype.todoUpdateDone = function(data) {
});
};
Sidebar.prototype.sidebarDropdownLoading = function(e) {
Sidebar.prototype.sidebarDropdownLoading = function() {
var $loading, $sidebarCollapsedIcon, i, img;
$sidebarCollapsedIcon = $(this)
.closest('.block')
......@@ -157,7 +157,7 @@ Sidebar.prototype.sidebarDropdownLoading = function(e) {
}
};
Sidebar.prototype.sidebarDropdownLoaded = function(e) {
Sidebar.prototype.sidebarDropdownLoaded = function() {
var $sidebarCollapsedIcon, i, img;
$sidebarCollapsedIcon = $(this)
.closest('.block')
......
/* eslint-disable no-return-assign, one-var, no-var, no-unused-vars, consistent-return, object-shorthand, prefer-template, class-methods-use-this, no-lonely-if, vars-on-top */
/* eslint-disable no-return-assign, one-var, no-var, consistent-return, object-shorthand, prefer-template, class-methods-use-this, no-lonely-if, vars-on-top */
import $ from 'jquery';
import { escape, throttle } from 'underscore';
import { s__, __, sprintf } from '~/locale';
import { s__, __ } from '~/locale';
import { getIdenticonBackgroundClass, getIdenticonTitle } from '~/helpers/avatar_helper';
import axios from './lib/utils/axios_utils';
import DropdownUtils from './filtered_search/dropdown_utils';
import {
isInGroupsPage,
isInProjectPage,
......@@ -142,7 +141,7 @@ export class SearchAutocomplete {
});
}
getSearchText(selectedObject, el) {
getSearchText(selectedObject) {
return selectedObject.id ? selectedObject.text : '';
}
......@@ -402,7 +401,7 @@ export class SearchAutocomplete {
return this.searchInput.val('').focus();
}
onSearchInputBlur(e) {
onSearchInputBlur() {
this.isFocused = false;
this.wrap.removeClass('search-active');
// If input is blank then restore state
......
/* eslint-disable func-names, one-var, no-var, prefer-rest-params, vars-on-top, prefer-arrow-callback, consistent-return, object-shorthand, no-shadow, no-unused-vars, no-else-return, no-self-compare, prefer-template, no-unused-expressions, yoda, prefer-spread, camelcase, no-param-reassign */
/* eslint-disable func-names, one-var, no-var, prefer-rest-params, vars-on-top, prefer-arrow-callback, consistent-return, object-shorthand, no-shadow, no-else-return, no-self-compare, prefer-template, no-unused-expressions, yoda, prefer-spread, camelcase, no-param-reassign */
/* global Issuable */
/* global emitSidebarEvent */
......@@ -405,7 +405,7 @@ function UsersSelect(currentUser, els, options = {}) {
}
},
defaultLabel: defaultLabel,
hidden: function(e) {
hidden: function() {
if ($dropdown.hasClass('js-multiselect')) {
emitSidebarEvent('sidebar.saveAssignees');
}
......@@ -442,7 +442,6 @@ function UsersSelect(currentUser, els, options = {}) {
if (user.beforeDivider && user.name.toLowerCase() === 'unassigned') {
// Unassigned selected
previouslySelected.each((index, element) => {
const id = parseInt(element.value, 10);
element.remove();
});
emitSidebarEvent('sidebar.removeAllAssignees');
......@@ -548,7 +547,7 @@ function UsersSelect(currentUser, els, options = {}) {
},
updateLabel: $dropdown.data('dropdownTitle'),
renderRow: function(user) {
var avatar, img, listClosingTags, listWithName, listWithUserName, username;
var avatar, img, username;
username = user.username ? '@' + user.username : '';
avatar = user.avatar_url ? user.avatar_url : gon.default_avatar_url;
......
/* eslint-disable func-names, prefer-arrow-callback, no-unused-vars, consistent-return, camelcase, class-methods-use-this */
/* eslint-disable func-names, prefer-arrow-callback, consistent-return, camelcase, class-methods-use-this */
// Zen Mode (full screen) textarea
//
......@@ -62,7 +62,7 @@ export default class ZenMode {
$(document).on(
'zen_mode:leave',
(function(_this) {
return function(e) {
return function() {
return _this.exit();
};
})(this),
......
......@@ -95,7 +95,7 @@ module Ci
# rubocop: disable CodeReuse/ActiveRecord
def auto_cancelable_pipelines
# TODO: Introduced by https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/23464
if Feature.enabled?(:ci_support_interruptible_pipelines, project, default_enabled: true)
if Feature.enabled?(:ci_support_interruptible_pipelines, project, default_enabled: false)
project.ci_pipelines
.where(ref: pipeline.ref)
.where.not(id: pipeline.id)
......
# frozen_string_literal: true
class ServiceResponse
def self.success(message: nil, payload: {})
new(status: :success, message: message, payload: payload)
def self.success(message: nil, payload: {}, http_status: :ok)
new(status: :success, message: message, payload: payload, http_status: http_status)
end
def self.error(message:, payload: {}, http_status: nil)
......
......@@ -29,4 +29,4 @@
= yield :push_access_levels
.card-footer
= f.submit 'Protect', class: 'btn-success btn', disabled: true
= f.submit 'Protect', class: 'btn-success btn', disabled: true, data: { qa_selector: 'protect_button' }
#!/usr/bin/env ruby
require 'rubygems'
require 'bundler/setup'
require 'json'
require 'active_model'
require 'active_support'
require 'active_support/core_ext'
require 'benchmark'
require 'charlock_holmes'
$: << File.expand_path('../lib', __dir__)
$: << File.expand_path('../ee/lib', __dir__)
require 'open3'
require 'rugged'
require 'gitlab/blob_helper'
require 'gitlab/elastic/client'
require 'elasticsearch/model'
require 'elasticsearch/git'
require 'elasticsearch/git/encoder_helper'
require 'elasticsearch/git/lite_blob'
require 'elasticsearch/git/model'
require 'elasticsearch/git/repository'
Thread.abort_on_exception = true
path_to_log_file = File.expand_path('../log/es-indexer.log', __dir__)
LOGGER = Logger.new(path_to_log_file)
PROJECT_ID = ARGV.shift
REPO_PATH = ARGV.shift
FROM_SHA = ENV['FROM_SHA']
TO_SHA = ENV['TO_SHA']
RAILS_ENV = ENV['RAILS_ENV']
# Symbols get stringified when passed through JSON
elastic = {}
JSON.parse(ENV['ELASTIC_CONNECTION_INFO']).each { |k, v| elastic[k.to_sym] = v }
ELASTIC_CONFIG = elastic
LOGGER.info("Has been scheduled for project #{REPO_PATH} with SHA range #{FROM_SHA}:#{TO_SHA}")
class Repository
include Elasticsearch::Git::Repository
index_name ['gitlab', RAILS_ENV].compact.join('-')
def initialize
self.__elasticsearch__.client = ::Gitlab::Elastic::Client.build(ELASTIC_CONFIG)
end
def client_for_indexing
self.__elasticsearch__.client
end
def repository_id
PROJECT_ID
end
def project_id
PROJECT_ID
end
def path_to_repo
REPO_PATH
end
end
repo = Repository.new
params = { from_rev: FROM_SHA, to_rev: TO_SHA }.compact
commit_thr = Thread.new do
LOGGER.info("Indexing commits started")
timings = Benchmark.measure do
indexed = 0
repo.index_commits(params) do |batch, total_count|
indexed += batch.length
LOGGER.info("Indexed #{indexed}/#{total_count} commits")
end
end
LOGGER.info("Commits for #{REPO_PATH} are indexed. Time elapsed: #{timings.real}")
end
LOGGER.info("Indexing blobs started")
timings = Benchmark.measure do
indexed = 0
repo.index_blobs(params) do |batch, total_count|
indexed += batch.length
LOGGER.info("Indexed #{indexed}/#{total_count} blobs")
end
end
LOGGER.info("Blobs for #{REPO_PATH} are indexed. Time elapsed: #{timings.real}")
commit_thr.join
---
title: Avoid Devise "401 Unauthorized" responses
merge_request: 16519
author:
type: fixed
......@@ -982,6 +982,10 @@ production: &base
# Default is '.gitlab_workhorse_secret' relative to Rails.root (i.e. root of the GitLab app).
# secret_file: /home/git/gitlab/.gitlab_workhorse_secret
## GitLab Elasticsearch settings
elasticsearch:
indexer_path: /home/git/gitlab-elasticsearch-indexer/
## Git settings
# CAUTION!
# Use the default values unless you really know what you are doing
......
......@@ -214,11 +214,9 @@ Devise.setup do |config|
# If you want to use other strategies, that are not supported by Devise, or
# change the failure app, you can configure them inside the config.warden block.
#
# config.warden do |manager|
# manager.failure_app = Gitlab::DeviseFailure
# manager.intercept_401 = false
# manager.default_strategies(scope: :user).unshift :some_external_strategy
# end
config.warden do |manager|
manager.failure_app = Gitlab::DeviseFailure
end
if Gitlab::Auth::LDAP::Config.enabled?
Gitlab::Auth::LDAP::Config.providers.each do |provider|
......
......@@ -37,6 +37,10 @@ class EmojiChecker
end
end
def gitlab_danger
@gitlab_danger ||= GitlabDanger.new(helper.gitlab_helper)
end
def fail_commit(commit, message)
fail("#{commit.sha}: #{message}")
end
......@@ -56,6 +60,8 @@ def subject_starts_with_capital?(subject)
end
def ce_upstream?
return unless gitlab_danger.ci?
gitlab.mr_labels.any? { |label| label == 'CE upstream' }
end
......@@ -88,8 +94,8 @@ def lint_commit(commit) # rubocop:disable Metrics/AbcSize
# We ignore revert commits as they are well structured by Git already
return false if commit.message.start_with?('Revert "')
is_squash = gitlab.mr_json['squash']
is_wip = gitlab.mr_json['work_in_progress']
is_squash = gitlab_danger.ci? ? gitlab.mr_json['squash'] : false
is_wip = gitlab_danger.ci? ? gitlab.mr_json['work_in_progress'] : false
is_fixup = commit.message.start_with?('fixup!', 'squash!')
if is_fixup
......
# frozen_string_literal: true
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddAnyApproverRuleUniqueIndexes < ActiveRecord::Migration[5.2]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
PROJECT_RULE_UNIQUE_INDEX = 'any_approver_project_rule_type_unique_index'
MERGE_REQUEST_RULE_UNIQUE_INDEX = 'any_approver_merge_request_rule_type_unique_index'
disable_ddl_transaction!
def up
add_concurrent_index(:approval_project_rules, [:project_id],
where: "rule_type = 3",
name: PROJECT_RULE_UNIQUE_INDEX, unique: true)
add_concurrent_index(:approval_merge_request_rules, [:merge_request_id, :rule_type],
where: "rule_type = 4",
name: MERGE_REQUEST_RULE_UNIQUE_INDEX, unique: true)
end
def down
remove_concurrent_index_by_name(:approval_project_rules, PROJECT_RULE_UNIQUE_INDEX)
remove_concurrent_index_by_name(:approval_merge_request_rules, MERGE_REQUEST_RULE_UNIQUE_INDEX)
end
end
# frozen_string_literal: true
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class ScheduleProjectAnyApprovalRuleMigration < ActiveRecord::Migration[5.2]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
BATCH_SIZE = 5_000
MIGRATION = 'PopulateAnyApprovalRuleForProjects'
DELAY_INTERVAL = 8.minutes.to_i
disable_ddl_transaction!
class Project < ActiveRecord::Base
include EachBatch
self.table_name = 'projects'
scope :with_approvals_before_merge, -> { where('approvals_before_merge <> 0') }
end
def up
add_concurrent_index :projects, :id,
name: 'tmp_projects_with_approvals_before_merge',
where: 'approvals_before_merge <> 0'
say "Scheduling `#{MIGRATION}` jobs"
# We currently have ~43k project records with non-zero approvals_before_merge on GitLab.com.
# This means it'll schedule ~9 jobs (5k projects each) with a 8 minutes gap,
# so this should take ~1 hour for all background migrations to complete.
#
# The approximate expected number of affected rows is: 18k
queue_background_migration_jobs_by_range_at_intervals(
ScheduleProjectAnyApprovalRuleMigration::Project.with_approvals_before_merge,
MIGRATION, DELAY_INTERVAL, batch_size: BATCH_SIZE)
remove_concurrent_index_by_name(:projects, 'tmp_projects_with_approvals_before_merge')
end
def down
# no-op
end
end
# frozen_string_literal: true
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class ScheduleMergeRequestAnyApprovalRuleMigration < ActiveRecord::Migration[5.2]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
BATCH_SIZE = 5_000
MIGRATION = 'PopulateAnyApprovalRuleForMergeRequests'
DELAY_INTERVAL = 8.minutes.to_i
disable_ddl_transaction!
class MergeRequest < ActiveRecord::Base
include EachBatch
self.table_name = 'merge_requests'
scope :with_approvals_before_merge, -> { where('approvals_before_merge <> 0') }
end
def up
add_concurrent_index :merge_requests, :id,
name: 'tmp_merge_requests_with_approvals_before_merge',
where: 'approvals_before_merge <> 0'
say "Scheduling `#{MIGRATION}` jobs"
# We currently have ~440_000 merge request records with non-zero approvals_before_merge on GitLab.com.
# This means it'll schedule ~88 jobs (5k merge requests each) with a 8 minutes gap,
# so this should take ~12 hours for all background migrations to complete.
#
# The approximate expected number of affected rows is: 190k
queue_background_migration_jobs_by_range_at_intervals(
ScheduleMergeRequestAnyApprovalRuleMigration::MergeRequest.with_approvals_before_merge,
MIGRATION, DELAY_INTERVAL, batch_size: BATCH_SIZE)
remove_concurrent_index_by_name(:merge_requests, 'tmp_merge_requests_with_approvals_before_merge')
end
def down
# no-op
end
end
......@@ -319,8 +319,9 @@ ActiveRecord::Schema.define(version: 2019_09_12_061145) do
t.integer "report_type", limit: 2
t.index ["merge_request_id", "code_owner", "name"], name: "approval_rule_name_index_for_code_owners", unique: true, where: "(code_owner = true)"
t.index ["merge_request_id", "code_owner"], name: "index_approval_merge_request_rules_1"
t.index ["merge_request_id", "rule_type", "name"], name: "index_approval_rule_name_for_code_owners_rule_type", unique: true, where: "(rule_type = 2)"
t.index ["merge_request_id", "rule_type"], name: "index_approval_rules_code_owners_rule_type", where: "(rule_type = 2)"
t.index ["merge_request_id", "name"], name: "index_approval_rule_name_for_code_owners_rule_type", unique: true, where: "(rule_type = 2)"
t.index ["merge_request_id", "rule_type"], name: "any_approver_merge_request_rule_type_unique_index", unique: true, where: "(rule_type = 4)"
t.index ["merge_request_id"], name: "index_approval_rules_code_owners_rule_type", where: "(rule_type = 2)"
end
create_table "approval_merge_request_rules_approved_approvers", force: :cascade do |t|
......@@ -351,6 +352,7 @@ ActiveRecord::Schema.define(version: 2019_09_12_061145) do
t.integer "approvals_required", limit: 2, default: 0, null: false
t.string "name", null: false
t.integer "rule_type", limit: 2, default: 0, null: false
t.index ["project_id"], name: "any_approver_project_rule_type_unique_index", unique: true, where: "(rule_type = 3)"
t.index ["project_id"], name: "index_approval_project_rules_on_project_id"
t.index ["rule_type"], name: "index_approval_project_rules_on_rule_type"
end
......
......@@ -220,7 +220,6 @@ are listed in the descriptions of the relevant settings.
| `elasticsearch_aws` | boolean | no | **(PREMIUM)** Enable the use of AWS hosted Elasticsearch |
| `elasticsearch_aws_region` | string | no | **(PREMIUM)** The AWS region the elasticsearch domain is configured |
| `elasticsearch_aws_secret_access_key` | string | no | **(PREMIUM)** AWS IAM secret access key |
| `elasticsearch_experimental_indexer` | boolean | no | **(PREMIUM)** Use the experimental elasticsearch indexer. More info: <https://gitlab.com/gitlab-org/gitlab-elasticsearch-indexer> |
| `elasticsearch_indexing` | boolean | no | **(PREMIUM)** Enable Elasticsearch indexing |
| `elasticsearch_limit_indexing` | boolean | no | **(PREMIUM)** Limit Elasticsearch to index certain namespaces and projects |
| `elasticsearch_namespace_ids` | array of integers | no | **(PREMIUM)** The namespaces to index via Elasticsearch if `elasticsearch_limit_indexing` is enabled. |
......
......@@ -59,7 +59,7 @@ Additionally, if you need large repos or multiple forks for testing, please cons
## How does it work?
The Elasticsearch integration depends on an external indexer. We ship a [ruby indexer](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/bin/elastic_repo_indexer) by default but are also working on an [indexer written in Go](https://gitlab.com/gitlab-org/gitlab-elasticsearch-indexer). The user must trigger the initial indexing via a rake task, but after this is done GitLab itself will trigger reindexing when required via `after_` callbacks on create, update, and destroy that are inherited from [/ee/app/models/concerns/elastic/application_search.rb](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/ee/app/models/concerns/elastic/application_search.rb).
The Elasticsearch integration depends on an external indexer. We ship an [indexer written in Go](https://gitlab.com/gitlab-org/gitlab-elasticsearch-indexer). The user must trigger the initial indexing via a rake task but, after this is done, GitLab itself will trigger reindexing when required via `after_` callbacks on create, update, and destroy that are inherited from [/ee/app/models/concerns/elastic/application_search.rb](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/ee/app/models/concerns/elastic/application_search.rb).
All indexing after the initial one is done via `ElasticIndexerWorker` (sidekiq jobs).
......
......@@ -10,24 +10,30 @@ Review Apps are automatically deployed by each pipeline, both in
```mermaid
graph TD
build-qa-image -.->|once the `prepare` stage is done| gitlab:assets:compile
review-build-cng -->|triggers a CNG-mirror pipeline and wait for it to be done| CNG-mirror
review-build-cng -.->|once the `test` stage is done| review-deploy
review-deploy -.->|once the `review` stage is done| review-qa-smoke
build-qa-image -->|once the `prepare` stage is done| gitlab:assets:compile
gitlab:assets:compile -->|once the `gitlab:assets:compile` job is done| review-build-cng
review-build-cng -.->|triggers a CNG-mirror pipeline and wait for it to be done| CNG-mirror
CNG-mirror -.->|polls until completed| review-build-cng
review-build-cng -->|once the `review-build-cng` job is done| review-deploy
review-deploy -->|once the `review-deploy` job is done| review-qa-smoke
subgraph "1. gitlab-ce/ee `prepare` stage"
build-qa-image
end
subgraph "2. gitlab-ce/ee `test` stage"
gitlab:assets:compile -->|plays dependent job once done| review-build-cng
gitlab:assets:compile
end
subgraph "3. gitlab-ce/ee `review` stage"
subgraph "3. gitlab-ce/ee `review-prepare` stage"
review-build-cng
end
subgraph "4. gitlab-ce/ee `review` stage"
review-deploy["review-deploy<br><br>Helm deploys the Review App using the Cloud<br/>Native images built by the CNG-mirror pipeline.<br><br>Cloud Native images are deployed to the `review-apps-ce` or `review-apps-ee`<br>Kubernetes (GKE) cluster, in the GCP `gitlab-review-apps` project."]
end
subgraph "4. gitlab-ce/ee `qa` stage"
subgraph "5. gitlab-ce/ee `qa` stage"
review-qa-smoke[review-qa-smoke<br><br>gitlab-qa runs the smoke suite against the Review App.]
end
......@@ -177,6 +183,25 @@ secure note named **gitlab-{ce,ee} Review App's root password**.
`review-qa-raise-e-12chm0-migrations.1-nqwtx`.
1. Click on the `Container logs` link.
### Diagnosing unhealthy review-app releases
If [Review App Stability](https://gitlab.com/gitlab-org/quality/team-tasks/issues/93) dips this may be a signal
that the `review-apps-ce/ee` cluster is unhealthy. Leading indicators may be healthcheck failures leading to restarts or majority failure for Review App deployments.
The following items may help diagnose this:
- [Instance group CPU Utilization in GCP](https://console.cloud.google.com/compute/instanceGroups/details/us-central1-a/gke-review-apps-ce-preemp-n1-standard-a4c9571c-grp?project=gitlab-review-apps&tab=monitoring&graph=GCE_CPU&duration=PT12H) - helpful to identify if nodes are problematic or the entire cluster is trending towards unhealthy
- [Instance Group size in GCP](https://console.cloud.google.com/compute/instanceGroups/details/us-central1-a/gke-review-apps-ce-preemp-n1-standard-a4c9571c-grp?project=gitlab-review-apps&tab=monitoring&graph=GCE_SIZE&duration=PT12H) - aids in identifying load spikes on the cluster. Kubernetes will add nodes up to 220 based on total resource requests.
- `kubectl top nodes --sort-by=cpu` - can identify if node spikes are common or load on specific nodes which may get rebalanced by the Kubernetes scheduler.
- `kubectl top pods --sort-by=cpu` -
- [K9s] - K9s is a powerful command line dashboard which allows you to filter by labels. This can help identify trends with apps exceeding the [review-app resource requests](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/scripts/review_apps/base-config.yaml). Kubernetes will schedule pods to nodes based on resource requests and allow for CPU usage up to the limits.
- In K9s you can sort or add filters by typing the `/` character
- `-lrelease=<review-app-slug>` - filters down to all pods for a release. This aids in determining what is having issues in a single deployment
- `-lapp=<app>` - filters down to all pods for a specific app. This aids in determining resource usage by app.
- You can scroll to a Kubernetes resource and hit `d`(describe), `s`(shell), `l`(logs) for a deeper inspection
![K9s](img/k9s.png)
### Troubleshoot a pending `dns-gitlab-review-app-external-dns` Deployment
#### Finding the problem
......@@ -266,6 +291,12 @@ find a way to limit it to only us.**
## Other resources
- [Review Apps integration for CE/EE (presentation)](https://docs.google.com/presentation/d/1QPLr6FO4LduROU8pQIPkX1yfGvD13GEJIBOenqoKxR8/edit?usp=sharing)
- [Stability issues](https://gitlab.com/gitlab-org/quality/team-tasks/issues/212)
### Helpful command line tools
- [K9s] - enables CLI dashboard across pods and enabling filtering by labels
- [Stern](https://github.com/wercker/stern) - enables cross pod log tailing based on label/field selectors
[charts-1068]: https://gitlab.com/gitlab-org/charts/gitlab/issues/1068
[gitlab-pipeline]: https://gitlab.com/gitlab-org/gitlab-ce/pipelines/44362587
......@@ -285,6 +316,7 @@ find a way to limit it to only us.**
[gitlab-ci-yml]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab-ci.yml
[gitlab-k8s-integration]: ../../user/project/clusters/index.md
[password-bug]: https://gitlab.com/gitlab-org/gitlab-ce/issues/53621
[K9s]: https://github.com/derailed/k9s
---
......
......@@ -585,6 +585,25 @@ You can specify a different Git repository by providing it as an extra parameter
sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse,https://example.com/gitlab-workhorse.git]" RAILS_ENV=production
```
### Install gitlab-elasticsearch-indexer
GitLab-Elasticsearch-Indexer uses [GNU Make](https://www.gnu.org/software/make/). The
following command-line will install GitLab-Elasticsearch-Indexer in `/home/git/gitlab-elasticsearch-indexer`
which is the recommended location.
```sh
sudo -u git -H bundle exec rake "gitlab:indexer:install[/home/git/gitlab-elasticsearch-indexer]" RAILS_ENV=production
```
You can specify a different Git repository by providing it as an extra parameter:
```sh
sudo -u git -H bundle exec rake "gitlab:indexer:install[/home/git/gitlab-elasticsearch-indexer,https://example.com/gitlab-elasticsearch-indexer.git]" RAILS_ENV=production
```
The source code will first be fetched to the path specified by the first parameter. Then a binary will be built under its `bin` directory.
You will then need to update `gitlab.yml`'s `production -> elasticsearch -> indexer_path` setting to point to that binary.
### Install GitLab Pages
GitLab Pages uses [GNU Make](https://www.gnu.org/software/make/). This step is optional and only needed if you wish to host static sites from within GitLab. The following commands will install GitLab Pages in `/home/git/gitlab-pages`. For additional setup steps, consult the [administration guide](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/administration/pages/source.md) for your version of GitLab as the GitLab Pages daemon can be run several different ways.
......
......@@ -58,7 +58,7 @@ The following languages and dependency managers are supported.
| JavaScript ([npm](https://www.npmjs.com/), [yarn](https://yarnpkg.com/en/)) | yes | [gemnasium](https://gitlab.com/gitlab-org/security-products/gemnasium), [Retire.js](https://retirejs.github.io/retire.js) |
| Go ([Golang](https://golang.org/)) | not currently ([issue](https://gitlab.com/gitlab-org/gitlab-ee/issues/7132 "Dependency Scanning for Go")) | not available |
| PHP ([Composer](https://getcomposer.org/)) | yes | [gemnasium](https://gitlab.com/gitlab-org/security-products/gemnasium) |
| Python ([pip](https://pip.pypa.io/en/stable/)) (only `requirements.txt` supported) | yes | [gemnasium](https://gitlab.com/gitlab-org/security-products/gemnasium) |
| Python ([pip](https://pip.pypa.io/en/stable/)) | yes | [gemnasium](https://gitlab.com/gitlab-org/security-products/gemnasium) |
| Python ([Pipfile](https://docs.pipenv.org/en/latest/basics/)) | not currently ([issue](https://gitlab.com/gitlab-org/gitlab-ee/issues/11756 "Pipfile.lock support for Dependency Scanning"))| not available |
| Python ([poetry](https://poetry.eustace.io/)) | not currently ([issue](https://gitlab.com/gitlab-org/gitlab-ee/issues/7006 "Support Poetry in Dependency Scanning")) | not available |
| Ruby ([gem](https://rubygems.org/)) | yes | [gemnasium](https://gitlab.com/gitlab-org/security-products/gemnasium), [bundler-audit](https://github.com/rubysec/bundler-audit) |
......
---
type: reference
---
# Time Tracking
> Introduced in GitLab 8.14.
......@@ -7,7 +11,7 @@ requests within GitLab.
## Overview
Time Tracking lets you:
Time Tracking allows you:
- 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
......@@ -18,7 +22,7 @@ You don't have to indicate an estimate to enter the time spent, and vice versa.
Data about time tracking is shown on the issue/merge request sidebar, as shown
below.
![Time tracking in the sidebar](time-tracking/time-tracking-sidebar.png)
![Time tracking in the sidebar](time_tracking/img/time_tracking_sidebar_v8_16.png)
## How to enter data
......@@ -30,7 +34,7 @@ in a comment in both an issue or a merge request.
Below is an example of how you can use those new quick actions inside a comment.
![Time tracking example in a comment](time-tracking/time-tracking-example.png)
![Time tracking example in a comment](time_tracking/img/time_tracking_example_v12_2.png)
Adding time entries (time spent or estimates) is limited to project members.
......@@ -65,11 +69,11 @@ To remove all the time spent at once, use `/remove_time_spent`.
The following time units are available:
- months (mo)
- weeks (w)
- days (d)
- hours (h)
- minutes (m)
- Months (mo)
- Weeks (w)
- Days (d)
- Hours (h)
- Minutes (m)
Default conversion rates are 1mo = 4w, 1w = 5d and 1d = 8h.
......
# frozen_string_literal: true
module Gitlab
class DeviseFailure < Devise::FailureApp
# If the request format is not known, send a redirect instead of a 401
# response, since this is the outcome we're most likely to want
def http_auth?
return super unless Feature.enabled?(:devise_redirect_unknown_formats, default_enabled: true)
request_format && super
end
end
end
......@@ -5,7 +5,7 @@ module Gitlab
extend self
# We may want to configure it through project settings in a future version.
CUSTOM_DAY_AND_WEEK_LENGTH = { hours_per_day: 8, days_per_month: 20 }.freeze
CUSTOM_DAY_AND_MONTH_LENGTH = { hours_per_day: 8, days_per_month: 20 }.freeze
def parse(string)
string = string.sub(/\A-/, '')
......@@ -14,7 +14,7 @@ module Gitlab
begin
ChronicDuration.parse(
string,
CUSTOM_DAY_AND_WEEK_LENGTH.merge(default_unit: 'hours'))
CUSTOM_DAY_AND_MONTH_LENGTH.merge(default_unit: 'hours'))
rescue
nil
end
......@@ -26,7 +26,7 @@ module Gitlab
def output(seconds)
ChronicDuration.output(
seconds,
CUSTOM_DAY_AND_WEEK_LENGTH.merge(
CUSTOM_DAY_AND_MONTH_LENGTH.merge(
format: :short,
limit_to_hours: limit_to_hours_setting,
weeks: true))
......
......@@ -10,13 +10,13 @@ class GitlabDanger
prettier
eslint
database
commit_messages
].freeze
CI_ONLY_RULES ||= %w[
metadata
changelog
specs
commit_messages
roulette
single_codebase
gitlab_ui_wg
......
......@@ -61,6 +61,7 @@ module QA
autoload :KubernetesCluster, 'qa/resource/kubernetes_cluster'
autoload :User, 'qa/resource/user'
autoload :ProjectMilestone, 'qa/resource/project_milestone'
autoload :Members, 'qa/resource/members'
autoload :Wiki, 'qa/resource/wiki'
autoload :File, 'qa/resource/file'
autoload :Fork, 'qa/resource/fork'
......
......@@ -44,7 +44,7 @@ module QA
def sign_in_using_credentials(user = nil)
# Don't try to log-in if we're already logged-in
return if Page::Main::Menu.perform { |menu| menu.has_personal_area?(wait: 0) }
return if Page::Main::Menu.perform(&:signed_in?)
using_wait_time 0 do
set_initial_password_if_present
......@@ -75,10 +75,7 @@ module QA
end
def sign_in_using_ldap_credentials(user)
# Log out if already logged in
Page::Main::Menu.perform do |menu|
menu.sign_out if menu.has_personal_area?(wait: 0)
end
Page::Main::Menu.perform(&:sign_out_if_signed_in)
using_wait_time 0 do
set_initial_password_if_present
......@@ -149,7 +146,7 @@ module QA
end
def sign_out_and_sign_in_as(user:)
Menu.perform(&:sign_out)
Menu.perform(&:sign_out_if_signed_in)
has_sign_in_tab?
sign_in_using_credentials(user)
end
......
......@@ -55,6 +55,10 @@ module QA
within_top_menu { click_element :admin_area_link }
end
def signed_in?
has_personal_area?(wait: 0)
end
def sign_out
within_user_menu do
click_element :sign_out_link
......@@ -62,7 +66,7 @@ module QA
end
def sign_out_if_signed_in
sign_out if has_personal_area?(wait: 0)
sign_out if signed_in?
end
def click_settings_link
......
......@@ -25,6 +25,10 @@ module QA
element :protected_branches_list
end
view 'app/views/projects/protected_branches/shared/_create_protected_branch.html.haml' do
element :protect_button
end
def select_branch(branch_name)
click_element :protected_branch_select
......@@ -33,40 +37,31 @@ module QA
end
end
def allow_no_one_to_push
go_to_allow(:push, 'No one')
end
def allow_devs_and_maintainers_to_push
go_to_allow(:push, 'Developers + Maintainers')
def select_allowed_to_merge(allowed)
select_allowed(:merge, allowed)
end
# @deprecated
alias_method :allow_devs_and_masters_to_push, :allow_devs_and_maintainers_to_push
def allow_no_one_to_merge
go_to_allow(:merge, 'No one')
def select_allowed_to_push(allowed)
select_allowed(:push, allowed)
end
def allow_devs_and_maintainers_to_merge
go_to_allow(:merge, 'Developers + Maintainers')
end
# @deprecated
alias_method :allow_devs_and_masters_to_merge, :allow_devs_and_maintainers_to_merge
def protect_branch
click_on 'Protect'
click_element :protect_button
end
private
def go_to_allow(action, text)
def select_allowed(action, allowed)
click_element :"allowed_to_#{action}_select"
allowed[:roles] = Resource::ProtectedBranch::Roles::NO_ONE unless allowed.key?(:roles)
within_element(:"allowed_to_#{action}_dropdown") do
click_on text
click_on allowed[:roles]
end
# Click the select element again to close the dropdown
click_element :protected_branch_select
end
end
end
......
......@@ -3,6 +3,8 @@
module QA
module Resource
class Group < Base
include Members
attr_accessor :path, :description
attribute :sandbox do
......@@ -48,19 +50,10 @@ module QA
super
end
def add_member(user, access_level = '30')
# 30 = developer access
post Runtime::API::Request.new(api_client, api_members_path).url, { user_id: user.id, access_level: access_level }
end
def api_get_path
"/groups/#{CGI.escape("#{sandbox.path}/#{path}")}"
end
def api_members_path
"#{api_get_path}/members"
end
def api_post_path
'/groups'
end
......
# frozen_string_literal: true
module QA
module Resource
#
# Included in Resource::Project and Resource::Group to allow changes to
# project/group membership
#
module Members
def add_member(user, access_level = AccessLevel::DEVELOPER)
post Runtime::API::Request.new(api_client, api_members_path).url, { user_id: user.id, access_level: access_level }
end
def api_members_path
"#{api_get_path}/members"
end
class AccessLevel
NO_ACCESS = 0
GUEST = 10
REPORTER = 20
DEVELOPER = 30
MAINTAINER = 40
OWNER = 50
end
end
end
end
......@@ -6,6 +6,7 @@ module QA
module Resource
class Project < Base
include Events::Project
include Members
attr_writer :initialize_with_readme
attr_writer :visibility
......@@ -75,11 +76,6 @@ module QA
super
end
def add_member(user, access_level = '30')
# 30 = developer access
post Runtime::API::Request.new(api_client, api_members_path).url, { user_id: user.id, access_level: access_level }
end
def api_get_path
"/projects/#{CGI.escape(path_with_namespace)}"
end
......@@ -112,6 +108,10 @@ module QA
post_body
end
def share_with_group(invitee, access_level = Resource::Members::AccessLevel::DEVELOPER)
post Runtime::API::Request.new(api_client, "/projects/#{id}/share").url, { group_id: invitee.id, group_access: access_level }
end
private
def transform_api_resource(api_resource)
......
......@@ -3,7 +3,7 @@
module QA
module Resource
class ProtectedBranch < Base
attr_accessor :branch_name, :allow_to_push, :allow_to_merge, :protected
attr_accessor :branch_name, :allowed_to_push, :allowed_to_merge, :protected
attribute :project do
Project.fabricate_via_api! do |resource|
......@@ -25,8 +25,12 @@ module QA
def initialize
@branch_name = 'test/branch'
@allow_to_push = true
@allow_to_merge = true
@allowed_to_push = {
roles: Resource::ProtectedBranch::Roles::DEVS_AND_MAINTAINERS
}
@allowed_to_merge = {
roles: Resource::ProtectedBranch::Roles::DEVS_AND_MAINTAINERS
}
@protected = false
end
......@@ -35,29 +39,14 @@ module QA
project.wait_for_push_new_branch @branch_name
# The upcoming process will make it access the Protected Branches page,
# select the already created branch and protect it according
# to `allow_to_push` variable.
return branch unless @protected
project.visit!
Page::Project::Menu.perform(&:go_to_repository_settings)
Page::Project::Settings::Repository.perform do |setting|
setting.expand_protected_branches do |page|
page.select_branch(branch_name)
if allow_to_push
page.allow_devs_and_maintainers_to_push
else
page.allow_no_one_to_push
end
if allow_to_merge
page.allow_devs_and_maintainers_to_merge
else
page.allow_no_one_to_merge
end
page.select_allowed_to_merge(allowed_to_merge)
page.select_allowed_to_push(allowed_to_push)
page.wait(reload: false) do
!page.first('.btn-success').disabled?
......@@ -79,6 +68,12 @@ module QA
def api_delete_path
"/projects/#{@project.api_resource[:id]}/protected_branches/#{@branch_name}"
end
class Roles
NO_ONE = 'No one'
DEVS_AND_MAINTAINERS = 'Developers + Maintainers'
MAINTAINERS = 'Maintainers'
end
end
end
end
......@@ -17,16 +17,11 @@ module QA
Page::Main::Login.perform(&:sign_in_using_credentials)
end
after do
# We need to clear localStorage because we're using it for the dropdown,
# and capybara doesn't do this for us.
# https://github.com/teamcapybara/capybara/issues/1702
Capybara.execute_script 'localStorage.clear()'
end
context 'when developers and maintainers are allowed to push to a protected branch' do
it 'user with push rights successfully pushes to the protected branch' do
create_protected_branch(allow_to_push: true)
create_protected_branch(allowed_to_push: {
roles: Resource::ProtectedBranch::Roles::DEVS_AND_MAINTAINERS
})
push = push_new_file(branch_name)
......@@ -36,18 +31,19 @@ module QA
context 'when developers and maintainers are not allowed to push to a protected branch' do
it 'user without push rights fails to push to the protected branch' do
create_protected_branch(allow_to_push: false)
create_protected_branch(allowed_to_push: {
roles: Resource::ProtectedBranch::Roles::NO_ONE
})
expect { push_new_file(branch_name) }.to raise_error(QA::Git::Repository::RepositoryCommandError, /remote: GitLab: You are not allowed to push code to protected branches on this project\.([\s\S]+)\[remote rejected\] #{branch_name} -> #{branch_name} \(pre-receive hook declined\)/)
end
end
def create_protected_branch(allow_to_push:)
def create_protected_branch(allowed_to_push:)
Resource::ProtectedBranch.fabricate! do |resource|
resource.branch_name = branch_name
resource.project = project
resource.allow_to_push = allow_to_push
resource.protected = true
resource.allowed_to_push = allowed_to_push
end
end
......
......@@ -171,16 +171,40 @@ describe ApplicationController do
end
describe '#route_not_found' do
controller(described_class) do
def index
route_not_found
end
end
it 'renders 404 if authenticated' do
allow(controller).to receive(:current_user).and_return(user)
expect(controller).to receive(:not_found)
controller.send(:route_not_found)
sign_in(user)
get :index
expect(response).to have_gitlab_http_status(404)
end
it 'does redirect to login page via authenticate_user! if not authenticated' do
allow(controller).to receive(:current_user).and_return(nil)
expect(controller).to receive(:authenticate_user!)
controller.send(:route_not_found)
it 'redirects to login page via authenticate_user! if not authenticated' do
get :index
expect(response).to redirect_to new_user_session_path
end
context 'request format is unknown' do
it 'redirects if unauthenticated' do
get :index, format: 'unknown'
expect(response).to redirect_to new_user_session_path
end
it 'returns a 401 if the feature flag is disabled' do
stub_feature_flags(devise_redirect_unknown_formats: false)
get :index, format: 'unknown'
expect(response).to have_gitlab_http_status(401)
end
end
end
......
/* eslint-disable no-unused-vars */
/* global ListIssue */
import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import Cookies from 'js-cookie';
......@@ -190,7 +188,7 @@ describe('Store', () => {
it('moves the position of lists', () => {
const listOne = boardsStore.addList(listObj);
const listTwo = boardsStore.addList(listObjDuplicate);
boardsStore.addList(listObjDuplicate);
expect(boardsStore.state.lists.length).toBe(2);
......
......@@ -87,4 +87,12 @@ describe Gitlab::Popen do
it { expect(@status).to be_zero }
it { expect(@output).to eq('hello') }
end
context 'when binary is absent' do
it 'raises error' do
expect do
@klass.new.popen(%w[foobar])
end.to raise_error
end
end
end
......@@ -9,7 +9,7 @@ describe GitlabDanger do
describe '.local_warning_message' do
it 'returns an informational message with rules that can run' do
expect(described_class.local_warning_message).to eq('==> Only the following Danger rules can be run locally: changes_size, gemfile, documentation, frozen_string, duplicate_yarn_dependencies, prettier, eslint, database')
expect(described_class.local_warning_message).to eq('==> Only the following Danger rules can be run locally: changes_size, gemfile, documentation, frozen_string, duplicate_yarn_dependencies, prettier, eslint, database, commit_messages')
end
end
......
......@@ -317,9 +317,14 @@ describe Ci::CreatePipelineService do
context 'interruptible builds' do
before do
Feature.enable(:ci_support_interruptible_pipelines)
stub_ci_pipeline_yaml_file(YAML.dump(config))
end
after do
Feature.disable(:ci_support_interruptible_pipelines)
end
let(:config) do
{
stages: %w[stage1 stage2 stage3 stage4],
......
......@@ -23,6 +23,20 @@ describe ServiceResponse do
expect(response).to be_success
expect(response.payload).to eq(good: 'orange')
end
it 'creates a successful response with default HTTP status' do
response = described_class.success
expect(response).to be_success
expect(response.http_status).to eq(:ok)
end
it 'creates a successful response with custom HTTP status' do
response = described_class.success(http_status: 204)
expect(response).to be_success
expect(response.http_status).to eq(204)
end
end
describe '.error' do
......
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