Commit 732750de authored by Valery Sizov's avatar Valery Sizov

Merge branch 'ce-to-ee-2017-10-13' into 'master'

CE upstream: Friday

Closes gitaly#586, gitlab-ce#38871, and gitaly#629

See merge request gitlab-org/gitlab-ee!3144
parents 046dca89 3a2aa432
......@@ -16,6 +16,7 @@ const Api = {
issuableTemplatePath: '/:namespace_path/:project_path/templates/:type/:key',
usersPath: '/api/:version/users.json',
commitPath: '/api/:version/projects/:id/repository/commits',
branchSinglePath: '/api/:version/projects/:id/repository/branches/:branch',
group(groupId, callback) {
const url = Api.buildUrl(Api.groupPath)
......@@ -124,6 +125,19 @@ const Api = {
});
},
branchSingle(id, branch) {
const url = Api.buildUrl(Api.branchSinglePath)
.replace(':id', id)
.replace(':branch', branch);
return this.wrapAjaxCall({
url,
type: 'GET',
contentType: 'application/json; charset=utf-8',
dataType: 'json',
});
},
// Return text for a specific license
licenseText(key, data, callback) {
const url = Api.buildUrl(Api.licensePath)
......
/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, no-else-return, object-shorthand, comma-dangle, max-len */
$(function() {
var previewPath;
$('input#broadcast_message_color').on('input', function() {
var previewColor;
previewColor = $(this).val();
return $('div.broadcast-message-preview').css('background-color', previewColor);
export default function initBroadcastMessagesForm() {
$('input#broadcast_message_color').on('input', function onMessageColorInput() {
const previewColor = $(this).val();
$('div.broadcast-message-preview').css('background-color', previewColor);
});
$('input#broadcast_message_font').on('input', function() {
var previewColor;
previewColor = $(this).val();
return $('div.broadcast-message-preview').css('color', previewColor);
$('input#broadcast_message_font').on('input', function onMessageFontInput() {
const previewColor = $(this).val();
$('div.broadcast-message-preview').css('color', previewColor);
});
previewPath = $('textarea#broadcast_message_message').data('preview-path');
return $('textarea#broadcast_message_message').on('input', function() {
var message;
message = $(this).val();
const previewPath = $('textarea#broadcast_message_message').data('preview-path');
$('textarea#broadcast_message_message').on('input', _.debounce(function onMessageInput() {
const message = $(this).val();
if (message === '') {
return $('.js-broadcast-message-preview').text("Your message here");
$('.js-broadcast-message-preview').text('Your message here');
} else {
return $.ajax({
$.ajax({
url: previewPath,
type: "POST",
type: 'POST',
data: {
broadcast_message: {
message: message
}
}
broadcast_message: { message },
},
});
}
});
});
}, 250));
}
......@@ -68,6 +68,7 @@ import initSettingsPanels from './settings_panels';
import initExperimentalFlags from './experimental_flags';
import OAuthRememberMe from './oauth_remember_me';
import PerformanceBar from './performance_bar';
import initBroadcastMessagesForm from './broadcast_message';
import initNotes from './init_notes';
import initLegacyFilters from './init_legacy_filters';
import initIssuableSidebar from './init_issuable_sidebar';
......@@ -78,11 +79,15 @@ import initChangesDropdown from './init_changes_dropdown';
import AbuseReports from './abuse_reports';
import { ajaxGet, convertPermissionToBoolean } from './lib/utils/common_utils';
import AjaxLoadingSpinner from './ajax_loading_spinner';
import GlFieldErrors from './gl_field_errors';
import GLForm from './gl_form';
import Shortcuts from './shortcuts';
import ShortcutsNavigation from './shortcuts_navigation';
import ShortcutsFindFile from './shortcuts_find_file';
import ShortcutsIssuable from './shortcuts_issuable';
import U2FAuthenticate from './u2f/authenticate';
import Members from './members';
import memberExpirationDate from './member_expiration_date';
// EE-only
import ApproversSelect from './approvers_select';
......@@ -252,7 +257,7 @@ import initGroupAnalytics from './init_group_analytics';
case 'groups:milestones:update':
new ZenMode();
new gl.DueDateSelectors();
new gl.GLForm($('.milestone-form'), true);
new GLForm($('.milestone-form'), true);
break;
case 'projects:compare:show':
new gl.Diff();
......@@ -269,7 +274,7 @@ import initGroupAnalytics from './init_group_analytics';
case 'projects:issues:new':
case 'projects:issues:edit':
shortcut_handler = new ShortcutsNavigation();
new gl.GLForm($('.issue-form'), true);
new GLForm($('.issue-form'), true);
new IssuableForm($('.issue-form'));
new LabelsSelect();
new MilestoneSelect();
......@@ -295,7 +300,7 @@ import initGroupAnalytics from './init_group_analytics';
case 'projects:merge_requests:edit':
new gl.Diff();
shortcut_handler = new ShortcutsNavigation();
new gl.GLForm($('.merge-request-form'), true);
new GLForm($('.merge-request-form'), true);
new IssuableForm($('.merge-request-form'));
new LabelsSelect();
new MilestoneSelect();
......@@ -304,7 +309,7 @@ import initGroupAnalytics from './init_group_analytics';
break;
case 'projects:tags:new':
new ZenMode();
new gl.GLForm($('.tag-form'), true);
new GLForm($('.tag-form'), true);
new RefSelectDropdown($('.js-branch-select'));
break;
case 'projects:snippets:show':
......@@ -314,17 +319,17 @@ import initGroupAnalytics from './init_group_analytics';
case 'projects:snippets:edit':
case 'projects:snippets:create':
case 'projects:snippets:update':
new gl.GLForm($('.snippet-form'), true);
new GLForm($('.snippet-form'), true);
break;
case 'snippets:new':
case 'snippets:edit':
case 'snippets:create':
case 'snippets:update':
new gl.GLForm($('.snippet-form'), false);
new GLForm($('.snippet-form'), false);
break;
case 'projects:releases:edit':
new ZenMode();
new gl.GLForm($('.release-form'), true);
new GLForm($('.release-form'), true);
break;
case 'projects:merge_requests:show':
new gl.Diff();
......@@ -430,15 +435,15 @@ import initGroupAnalytics from './init_group_analytics';
new ProjectsList();
break;
case 'groups:group_members:index':
new gl.MemberExpirationDate();
new gl.Members();
memberExpirationDate();
new Members();
new UsersSelect();
break;
case 'projects:project_members:index':
new gl.MemberExpirationDate('.js-access-expiration-date-groups');
memberExpirationDate('.js-access-expiration-date-groups');
new GroupsSelect();
new gl.MemberExpirationDate();
new gl.Members();
memberExpirationDate();
new Members();
new UsersSelect();
break;
case 'groups:new':
......@@ -621,6 +626,9 @@ import initGroupAnalytics from './init_group_analytics';
case 'admin':
new Admin();
switch (path[1]) {
case 'broadcast_messages':
initBroadcastMessagesForm();
break;
case 'cohorts':
new UsagePing();
break;
......@@ -684,7 +692,7 @@ import initGroupAnalytics from './init_group_analytics';
new Wikis();
shortcut_handler = new ShortcutsWiki();
new ZenMode();
new gl.GLForm($('.wiki-form'), true);
new GLForm($('.wiki-form'), true);
break;
case 'snippets':
shortcut_handler = new ShortcutsNavigation();
......@@ -709,12 +717,6 @@ import initGroupAnalytics from './init_group_analytics';
shortcut_handler = new ShortcutsNavigation();
}
break;
case 'users':
const action = path[1];
import(/* webpackChunkName: 'user_profile' */ './users')
.then(user => user.default(action))
.catch(() => {});
break;
}
// If we haven't installed a custom shortcut handler, install the default one
if (!shortcut_handler) {
......@@ -735,7 +737,7 @@ import initGroupAnalytics from './init_group_analytics';
Dispatcher.prototype.initFieldErrors = function() {
$('.gl-show-field-errors').each((i, form) => {
new gl.GlFieldErrors(form);
new GlFieldErrors(form);
});
};
......
......@@ -54,7 +54,7 @@ const inputErrorClass = 'gl-field-error-outline';
const errorAnchorSelector = '.gl-field-error-anchor';
const ignoreInputSelector = '.gl-field-error-ignore';
class GlFieldError {
export default class GlFieldError {
constructor({ input, formErrors }) {
this.inputElement = $(input);
this.inputDomElement = this.inputElement.get(0);
......@@ -159,6 +159,3 @@ class GlFieldError {
this.fieldErrorElement.hide();
}
}
window.gl = window.gl || {};
window.gl.GlFieldError = GlFieldError;
/* eslint-disable comma-dangle, class-methods-use-this, max-len, space-before-function-paren, arrow-parens, no-param-reassign */
import './gl_field_error';
import GlFieldError from './gl_field_error';
const customValidationFlag = 'gl-field-error-ignore';
class GlFieldErrors {
export default class GlFieldErrors {
constructor(form) {
this.form = $(form);
this.state = {
inputs: [],
valid: false
valid: false,
};
this.initValidators();
}
initValidators () {
initValidators() {
// register selectors here as needed
const validateSelectors = [':text', ':password', '[type=email]']
.map((selector) => `input${selector}`).join(',');
.map(selector => `input${selector}`).join(',');
this.state.inputs = this.form.find(validateSelectors).toArray()
.filter((input) => !input.classList.contains(customValidationFlag))
.map((input) => new window.gl.GlFieldError({ input, formErrors: this }));
.filter(input => !input.classList.contains(customValidationFlag))
.map(input => new GlFieldError({ input, formErrors: this }));
this.form.on('submit', this.catchInvalidFormSubmit);
this.form.on('submit', GlFieldErrors.catchInvalidFormSubmit);
}
/* Neccessary to prevent intercept and override invalid form submit
* because Safari & iOS quietly allow form submission when form is invalid
* and prevents disabling of invalid submit button by application.js */
catchInvalidFormSubmit (event) {
const $form = $(event.currentTarget);
static catchInvalidFormSubmit(e) {
const $form = $(e.currentTarget);
if (!$form.attr('novalidate')) {
if (!event.currentTarget.checkValidity()) {
event.preventDefault();
event.stopPropagation();
if (!e.currentTarget.checkValidity()) {
e.preventDefault();
e.stopPropagation();
}
}
}
......@@ -50,11 +48,9 @@ class GlFieldErrors {
});
}
focusOnFirstInvalid () {
const firstInvalid = this.state.inputs.filter((input) => !input.inputDomElement.validity.valid)[0];
focusOnFirstInvalid() {
const firstInvalid = this.state.inputs
.filter(input => !input.inputDomElement.validity.valid)[0];
firstInvalid.inputElement.focus();
}
}
window.gl = window.gl || {};
window.gl.GlFieldErrors = GlFieldErrors;
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-new, max-len */
/* global GitLab */
/* global DropzoneInput */
/* global autosize */
import GfmAutoComplete from './gfm_auto_complete';
window.gl = window.gl || {};
function GLForm(form, enableGFM = false) {
this.form = form;
this.textarea = this.form.find('textarea.js-gfm-input');
this.enableGFM = enableGFM;
// Before we start, we should clean up any previous data for this form
this.destroy();
// Setup the form
this.setupForm();
this.form.data('gl-form', this);
}
GLForm.prototype.destroy = function() {
// Clean form listeners
this.clearEventListeners();
if (this.autoComplete) {
this.autoComplete.destroy();
export default class GLForm {
constructor(form, enableGFM = false) {
this.form = form;
this.textarea = this.form.find('textarea.js-gfm-input');
this.enableGFM = enableGFM;
// Before we start, we should clean up any previous data for this form
this.destroy();
// Setup the form
this.setupForm();
this.form.data('gl-form', this);
}
return this.form.data('gl-form', null);
};
GLForm.prototype.setupForm = function() {
var isNewForm;
isNewForm = this.form.is(':not(.gfm-form)');
this.form.removeClass('js-new-note-form');
if (isNewForm) {
this.form.find('.div-dropzone').remove();
this.form.addClass('gfm-form');
// remove notify commit author checkbox for non-commit notes
gl.utils.disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button, .js-note-new-discussion'));
this.autoComplete = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources);
this.autoComplete.setup(this.form.find('.js-gfm-input'), {
emojis: true,
members: this.enableGFM,
issues: this.enableGFM,
milestones: this.enableGFM,
mergeRequests: this.enableGFM,
labels: this.enableGFM,
});
new DropzoneInput(this.form);
autosize(this.textarea);
destroy() {
// Clean form listeners
this.clearEventListeners();
if (this.autoComplete) {
this.autoComplete.destroy();
}
this.form.data('gl-form', null);
}
// form and textarea event listeners
this.addEventListeners();
gl.text.init(this.form);
// hide discard button
this.form.find('.js-note-discard').hide();
this.form.show();
if (this.isAutosizeable) this.setupAutosize();
};
GLForm.prototype.setupAutosize = function () {
this.textarea.off('autosize:resized')
.on('autosize:resized', this.setHeightData.bind(this));
setupForm() {
const isNewForm = this.form.is(':not(.gfm-form)');
this.form.removeClass('js-new-note-form');
if (isNewForm) {
this.form.find('.div-dropzone').remove();
this.form.addClass('gfm-form');
// remove notify commit author checkbox for non-commit notes
gl.utils.disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button, .js-note-new-discussion'));
this.autoComplete = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources);
this.autoComplete.setup(this.form.find('.js-gfm-input'), {
emojis: true,
members: this.enableGFM,
issues: this.enableGFM,
milestones: this.enableGFM,
mergeRequests: this.enableGFM,
labels: this.enableGFM,
});
new DropzoneInput(this.form); // eslint-disable-line no-new
autosize(this.textarea);
}
// form and textarea event listeners
this.addEventListeners();
gl.text.init(this.form);
// hide discard button
this.form.find('.js-note-discard').hide();
this.form.show();
if (this.isAutosizeable) this.setupAutosize();
}
this.textarea.off('mouseup.autosize')
.on('mouseup.autosize', this.destroyAutosize.bind(this));
setupAutosize() {
this.textarea.off('autosize:resized')
.on('autosize:resized', this.setHeightData.bind(this));
setTimeout(() => {
autosize(this.textarea);
this.textarea.css('resize', 'vertical');
}, 0);
};
this.textarea.off('mouseup.autosize')
.on('mouseup.autosize', this.destroyAutosize.bind(this));
GLForm.prototype.setHeightData = function () {
this.textarea.data('height', this.textarea.outerHeight());
};
setTimeout(() => {
autosize(this.textarea);
this.textarea.css('resize', 'vertical');
}, 0);
}
GLForm.prototype.destroyAutosize = function () {
const outerHeight = this.textarea.outerHeight();
setHeightData() {
this.textarea.data('height', this.textarea.outerHeight());
}
if (this.textarea.data('height') === outerHeight) return;
destroyAutosize() {
const outerHeight = this.textarea.outerHeight();
autosize.destroy(this.textarea);
if (this.textarea.data('height') === outerHeight) return;
this.textarea.data('height', outerHeight);
this.textarea.outerHeight(outerHeight);
this.textarea.css('max-height', window.outerHeight);
};
autosize.destroy(this.textarea);
GLForm.prototype.clearEventListeners = function() {
this.textarea.off('focus');
this.textarea.off('blur');
return gl.text.removeListeners(this.form);
};
this.textarea.data('height', outerHeight);
this.textarea.outerHeight(outerHeight);
this.textarea.css('max-height', window.outerHeight);
}
GLForm.prototype.addEventListeners = function() {
this.textarea.on('focus', function() {
return $(this).closest('.md-area').addClass('is-focused');
});
return this.textarea.on('blur', function() {
return $(this).closest('.md-area').removeClass('is-focused');
});
};
clearEventListeners() {
this.textarea.off('focus');
this.textarea.off('blur');
gl.text.removeListeners(this.form);
}
window.gl.GLForm = GLForm;
addEventListeners() {
this.textarea.on('focus', function focusTextArea() {
$(this).closest('.md-area').addClass('is-focused');
});
this.textarea.on('blur', function blurTextArea() {
$(this).closest('.md-area').removeClass('is-focused');
});
}
}
......@@ -85,7 +85,7 @@ w.gl.utils.getLocationHash = function(url) {
return hashIndex === -1 ? null : url.substring(hashIndex + 1);
};
w.gl.utils.refreshCurrentPage = () => gl.utils.visitUrl(document.location.href);
w.gl.utils.refreshCurrentPage = () => gl.utils.visitUrl(window.location.href);
// eslint-disable-next-line import/prefer-default-export
export function visitUrl(url, external = false) {
......@@ -96,7 +96,7 @@ export function visitUrl(url, external = false) {
otherWindow.opener = null;
otherWindow.location = url;
} else {
document.location.href = url;
window.location.href = url;
}
}
......
......@@ -44,7 +44,6 @@ import './aside';
import './autosave';
import loadAwardsHandler from './awards_handler';
import bp from './breakpoints';
import './broadcast_message';
import './commits';
import './compare';
import './compare_autocomplete';
......@@ -75,8 +74,6 @@ import './layout_nav';
import LazyLoader from './lazy_loader';
import './line_highlighter';
import './logo';
import './member_expiration_date';
import './members';
import './merge_request';
import './merge_request_tabs';
import './milestone';
......
......@@ -2,54 +2,51 @@
import Pikaday from 'pikaday';
(() => {
// Add datepickers to all `js-access-expiration-date` elements. If those elements are
// children of an element with the `clearable-input` class, and have a sibling
// `js-clear-input` element, then show that element when there is a value in the
// datepicker, and make clicking on that element clear the field.
//
window.gl = window.gl || {};
gl.MemberExpirationDate = (selector = '.js-access-expiration-date') => {
function toggleClearInput() {
$(this).closest('.clearable-input').toggleClass('has-value', $(this).val() !== '');
}
const inputs = $(selector);
inputs.each((i, el) => {
const $input = $(el);
const calendar = new Pikaday({
field: $input.get(0),
theme: 'gitlab-theme animate-picker',
format: 'yyyy-mm-dd',
minDate: new Date(),
container: $input.parent().get(0),
onSelect(dateText) {
$input.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
$input.trigger('change');
toggleClearInput.call($input);
},
});
calendar.setDate(new Date($input.val()));
$input.data('pikaday', calendar);
// Add datepickers to all `js-access-expiration-date` elements. If those elements are
// children of an element with the `clearable-input` class, and have a sibling
// `js-clear-input` element, then show that element when there is a value in the
// datepicker, and make clicking on that element clear the field.
//
export default function memberExpirationDate(selector = '.js-access-expiration-date') {
function toggleClearInput() {
$(this).closest('.clearable-input').toggleClass('has-value', $(this).val() !== '');
}
const inputs = $(selector);
inputs.each((i, el) => {
const $input = $(el);
const calendar = new Pikaday({
field: $input.get(0),
theme: 'gitlab-theme animate-picker',
format: 'yyyy-mm-dd',
minDate: new Date(),
container: $input.parent().get(0),
onSelect(dateText) {
$input.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
$input.trigger('change');
toggleClearInput.call($input);
},
});
inputs.next('.js-clear-input').on('click', function clicked(event) {
event.preventDefault();
calendar.setDate(new Date($input.val()));
$input.data('pikaday', calendar);
});
const input = $(this).closest('.clearable-input').find(selector);
const calendar = input.data('pikaday');
inputs.next('.js-clear-input').on('click', function clicked(event) {
event.preventDefault();
calendar.setDate(null);
input.trigger('change');
toggleClearInput.call(input);
});
const input = $(this).closest('.clearable-input').find(selector);
const calendar = input.data('pikaday');
calendar.setDate(null);
input.trigger('change');
toggleClearInput.call(input);
});
inputs.on('blur', toggleClearInput);
inputs.on('blur', toggleClearInput);
inputs.each(toggleClearInput);
};
}).call(window);
inputs.each(toggleClearInput);
}
/* eslint-disable class-methods-use-this, promise/catch-or-return */
/* eslint-disable no-new */
import Flash from './flash';
(() => {
window.gl = window.gl || {};
class Members {
constructor() {
this.addListeners();
this.initGLDropdown();
}
addListeners() {
$('.js-ldap-permissions').off('click').on('click', this.showLDAPPermissionsWarning.bind(this));
$('.js-ldap-override').off('click').on('click', this.toggleMemberAccessToggle.bind(this));
$('.project_member, .group_member').off('ajax:success').on('ajax:success', this.removeRow);
$('.js-member-update-control').off('change').on('change', this.formSubmit.bind(this));
$('.js-edit-member-form').off('ajax:success').on('ajax:success', this.formSuccess.bind(this));
gl.utils.disableButtonIfEmptyField('#user_ids', 'input[name=commit]', 'change');
}
initGLDropdown() {
$('.js-member-permissions-dropdown').each((i, btn) => {
const $btn = $(btn);
$btn.glDropdown({
selectable: true,
isSelectable(selected, $el) {
if ($el.data('revert')) {
return false;
}
return !$el.hasClass('is-active');
},
fieldName: $btn.data('field-name'),
id(selected, $el) {
return $el.data('id');
},
toggleLabel(selected, $el) {
if ($el.data('revert')) {
return $btn.text();
}
return $el.text();
},
clicked: (options) => {
const $link = options.$el;
if (!$link.data('revert')) {
this.formSubmit(null, $link);
} else {
const { $memberListItem, $toggle, $dateInput } = this.getMemberListItems($link);
$toggle.disable();
$dateInput.disable();
this.overrideLdap($memberListItem, $link.data('endpoint'), false).fail(() => {
$toggle.enable();
$dateInput.enable();
});
}
},
});
});
}
export default class Members {
constructor() {
this.addListeners();
this.initGLDropdown();
}
removeRow(e) {
const $target = $(e.target);
addListeners() {
$('.js-ldap-permissions').off('click').on('click', this.showLDAPPermissionsWarning.bind(this));
$('.js-ldap-override').off('click').on('click', this.toggleMemberAccessToggle.bind(this));
$('.project_member, .group_member').off('ajax:success').on('ajax:success', this.removeRow);
$('.js-member-update-control').off('change').on('change', this.formSubmit.bind(this));
$('.js-edit-member-form').off('ajax:success').on('ajax:success', this.formSuccess.bind(this));
gl.utils.disableButtonIfEmptyField('#user_ids', 'input[name=commit]', 'change');
}
if ($target.hasClass('btn-remove')) {
$target.closest('.member')
.fadeOut(function fadeOutMemberRow() {
$(this).remove();
});
}
}
initGLDropdown() {
$('.js-member-permissions-dropdown').each((i, btn) => {
const $btn = $(btn);
formSubmit(e, $el = null) {
const $this = e ? $(e.currentTarget) : $el;
const { $toggle, $dateInput } = this.getMemberListItems($this);
$btn.glDropdown({
selectable: true,
isSelectable(selected, $el) {
if ($el.data('revert')) {
return false;
}
$this.closest('form').trigger('submit.rails');
return !$el.hasClass('is-active');
},
fieldName: $btn.data('field-name'),
id(selected, $el) {
return $el.data('id');
},
toggleLabel(selected, $el) {
if ($el.data('revert')) {
return $btn.text();
}
$toggle.disable();
$dateInput.disable();
return $el.text();
},
clicked: (options) => {
const $link = options.$el;
if (!$link.data('revert')) {
this.formSubmit(null, $link);
} else {
const { $memberListItem, $toggle, $dateInput } = this.getMemberListItems($link);
$toggle.disable();
$dateInput.disable();
this.overrideLdap($memberListItem, $link.data('endpoint'), false).fail(() => {
$toggle.enable();
$dateInput.enable();
});
}
},
});
});
}
// eslint-disable-next-line class-methods-use-this
removeRow(e) {
const $target = $(e.target);
if ($target.hasClass('btn-remove')) {
$target.closest('.member')
.fadeOut(function fadeOutMemberRow() {
$(this).remove();
});
}
}
formSuccess(e) {
const { $toggle, $dateInput } = this.getMemberListItems($(e.currentTarget).closest('.member'));
$toggle.enable();
$dateInput.enable();
}
formSubmit(e, $el = null) {
const $this = e ? $(e.currentTarget) : $el;
const { $toggle, $dateInput } = this.getMemberListItems($this);
showLDAPPermissionsWarning(e) {
const $btn = $(e.currentTarget);
const { $memberListItem } = this.getMemberListItems($btn);
const $ldapPermissionsElement = $memberListItem.next();
$this.closest('form').trigger('submit.rails');
$ldapPermissionsElement.toggle();
}
$toggle.disable();
$dateInput.disable();
}
getMemberListItems($el) {
const $memberListItem = $el.is('.member') ? $el : $(`#${$el.data('el-id')}`);
formSuccess(e) {
const { $toggle, $dateInput } = this.getMemberListItems($(e.currentTarget).closest('.member'));
return {
$memberListItem,
$toggle: $memberListItem.find('.dropdown-menu-toggle'),
$dateInput: $memberListItem.find('.js-access-expiration-date'),
};
}
$toggle.enable();
$dateInput.enable();
}
toggleMemberAccessToggle(e) {
const $btn = $(e.currentTarget);
const { $memberListItem, $toggle, $dateInput } = this.getMemberListItems($btn);
showLDAPPermissionsWarning(e) {
const $btn = $(e.currentTarget);
const { $memberListItem } = this.getMemberListItems($btn);
const $ldapPermissionsElement = $memberListItem.next();
$btn.disable();
$ldapPermissionsElement.toggle();
}
// eslint-disable-next-line class-methods-use-this
getMemberListItems($el) {
const $memberListItem = $el.is('.member') ? $el : $(`#${$el.data('el-id')}`);
return {
$memberListItem,
$toggle: $memberListItem.find('.dropdown-menu-toggle'),
$dateInput: $memberListItem.find('.js-access-expiration-date'),
};
}
this.overrideLdap($memberListItem, $btn.data('endpoint'), true).then(() => {
this.showLDAPPermissionsWarning(e);
toggleMemberAccessToggle(e) {
const $btn = $(e.currentTarget);
const { $memberListItem, $toggle, $dateInput } = this.getMemberListItems($btn);
$toggle.enable();
$dateInput.enable();
}).fail((xhr) => {
$btn.enable();
$btn.disable();
// eslint-disable-next-line promise/catch-or-return
this.overrideLdap($memberListItem, $btn.data('endpoint'), true).then(() => {
this.showLDAPPermissionsWarning(e);
if (xhr.status === 403) {
new Flash('You do not have the correct permissions to override the settings from the LDAP group sync.', 'alert');
} else {
new Flash('An error occured whilst saving LDAP override status. Please try again.', 'alert');
}
});
}
$toggle.enable();
$dateInput.enable();
}).fail((xhr) => {
$btn.enable();
overrideLdap($memberListitem, endpoint, override) {
return $.ajax({
url: endpoint,
type: 'PATCH',
data: {
group_member: {
override,
},
if (xhr.status === 403) {
Flash('You do not have the correct permissions to override the settings from the LDAP group sync.', 'alert');
} else {
Flash('An error occured whilst saving LDAP override status. Please try again.', 'alert');
}
});
}
// eslint-disable-next-line class-methods-use-this
overrideLdap($memberListitem, endpoint, override) {
return $.ajax({
url: endpoint,
type: 'PATCH',
data: {
group_member: {
override,
},
}).then(() => {
$memberListitem.toggleClass('is-overriden', override);
});
}
},
}).then(() => {
$memberListitem.toggleClass('is-overriden', override);
});
}
gl.Members = Members;
})();
}
......@@ -19,6 +19,7 @@ import 'vendor/jquery.atwho';
import AjaxCache from '~/lib/utils/ajax_cache';
import Flash from './flash';
import CommentTypeToggle from './comment_type_toggle';
import GLForm from './gl_form';
import loadAwardsHandler from './awards_handler';
import './autosave';
import './dropzone_input';
......@@ -557,7 +558,7 @@ export default class Notes {
*/
setupNoteForm(form) {
var textarea, key;
new gl.GLForm(form, this.enableGFM);
this.glForm = new GLForm(form, this.enableGFM);
textarea = form.find('.js-note-text');
key = [
'Note',
......@@ -1152,7 +1153,7 @@ export default class Notes {
var targetId = $originalContentEl.data('target-id');
var targetType = $originalContentEl.data('target-type');
new gl.GLForm($editForm.find('form'), this.enableGFM);
this.glForm = new GLForm($editForm.find('form'), this.enableGFM);
$editForm.find('form')
.attr('action', postUrl)
......
import Vue from 'vue';
import Translate from '../vue_shared/translate';
import GlFieldErrors from '../gl_field_errors';
import intervalPatternInput from './components/interval_pattern_input.vue';
import TimezoneDropdown from './components/timezone_dropdown';
import TargetBranchDropdown from './components/target_branch_dropdown';
......@@ -39,7 +40,7 @@ document.addEventListener('DOMContentLoaded', () => {
gl.timezoneDropdown = new TimezoneDropdown();
gl.targetBranchDropdown = new TargetBranchDropdown();
gl.pipelineScheduleFieldErrors = new gl.GlFieldErrors(formElement);
gl.pipelineScheduleFieldErrors = new GlFieldErrors(formElement);
setupPipelineVariableList($('.js-pipeline-variable-list'));
});
......@@ -81,7 +81,11 @@ export default class PrometheusMetrics {
loadActiveMetrics() {
this.showMonitoringMetricsPanelState(PANEL_STATE.LOADING);
backOff((next, stop) => {
$.getJSON(this.activeMetricsEndpoint)
$.ajax({
url: this.activeMetricsEndpoint,
dataType: 'json',
global: false,
})
.done((res) => {
if (res && res.success) {
stop(res);
......
......@@ -3,11 +3,17 @@ import Flash from '../../flash';
import Store from '../stores/repo_store';
import RepoMixin from '../mixins/repo_mixin';
import Service from '../services/repo_service';
import PopupDialog from '../../vue_shared/components/popup_dialog.vue';
import { visitUrl } from '../../lib/utils/url_utility';
export default {
mixins: [RepoMixin],
data: () => Store,
mixins: [RepoMixin],
components: {
PopupDialog,
},
computed: {
showCommitable() {
......@@ -28,7 +34,16 @@ export default {
},
methods: {
makeCommit() {
commitToNewBranch(status) {
if (status) {
this.showNewBranchDialog = false;
this.tryCommit(null, true, true);
} else {
// reset the state
}
},
makeCommit(newBranch) {
// see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions
const commitMessage = this.commitMessage;
const actions = this.changedFiles.map(f => ({
......@@ -36,19 +51,63 @@ export default {
file_path: f.path,
content: f.newContent,
}));
const branch = newBranch ? `${this.currentBranch}-${this.currentShortHash}` : this.currentBranch;
const payload = {
branch: Store.currentBranch,
branch,
commit_message: commitMessage,
actions,
};
Store.submitCommitsLoading = true;
if (newBranch) {
payload.start_branch = this.currentBranch;
}
this.submitCommitsLoading = true;
Service.commitFiles(payload)
.then(this.resetCommitState)
.catch(() => Flash('An error occurred while committing your changes'));
.then(() => {
this.resetCommitState();
if (this.startNewMR) {
this.redirectToNewMr(branch);
} else {
this.redirectToBranch(branch);
}
})
.catch(() => {
Flash('An error occurred while committing your changes');
});
},
tryCommit(e, skipBranchCheck = false, newBranch = false) {
if (skipBranchCheck) {
this.makeCommit(newBranch);
} else {
Store.setBranchHash()
.then(() => {
if (Store.branchChanged) {
Store.showNewBranchDialog = true;
return;
}
this.makeCommit(newBranch);
})
.catch(() => {
Flash('An error occurred while committing your changes');
});
}
},
redirectToNewMr(branch) {
visitUrl(this.newMrTemplateUrl.replace('{{source_branch}}', branch));
},
redirectToBranch(branch) {
visitUrl(this.customBranchURL.replace('{{branch}}', branch));
},
resetCommitState() {
this.submitCommitsLoading = false;
this.openedFiles = this.openedFiles.map((file) => {
const f = file;
f.changed = false;
return f;
});
this.changedFiles = [];
this.commitMessage = '';
this.editMode = false;
......@@ -62,9 +121,17 @@ export default {
<div
v-if="showCommitable"
id="commit-area">
<popup-dialog
v-if="showNewBranchDialog"
:primary-button-label="__('Create new branch')"
kind="primary"
:title="__('Branch has changed')"
:text="__('This branch has changed since you started editing. Would you like to create a new branch?')"
@submit="commitToNewBranch"
/>
<form
class="form-horizontal"
@submit.prevent="makeCommit">
@submit.prevent="tryCommit">
<fieldset>
<div class="form-group">
<label class="col-md-4 control-label staged-files">
......@@ -117,7 +184,7 @@ export default {
class="btn btn-success">
<i
v-if="submitCommitsLoading"
class="fa fa-spinner fa-spin"
class="js-commit-loading-icon fa fa-spinner fa-spin"
aria-hidden="true"
aria-label="loading">
</i>
......@@ -126,6 +193,14 @@ export default {
</span>
</button>
</div>
<div class="col-md-offset-4 col-md-6">
<div class="checkbox">
<label>
<input type="checkbox" v-model="startNewMR">
<span>Start a <strong>new merge request</strong> with these changes</span>
</label>
</div>
</div>
</fieldset>
</form>
</div>
......
......@@ -31,8 +31,11 @@ function setInitialStore(data) {
Store.projectUrl = data.projectUrl;
Store.canCommit = data.canCommit;
Store.onTopOfBranch = data.onTopOfBranch;
Store.newMrTemplateUrl = decodeURIComponent(data.newMrTemplateUrl);
Store.customBranchURL = decodeURIComponent(data.blobUrl);
Store.currentBranch = $('button.dropdown-menu-toggle').attr('data-ref');
Store.checkIsCommitable();
Store.setBranchHash();
}
function initRepo(el) {
......
......@@ -64,6 +64,10 @@ const RepoService = {
return urlArray.join('/');
},
getBranch() {
return Api.branchSingle(Store.projectId, Store.currentBranch);
},
commitFiles(payload) {
return Api.commitMultiple(Store.projectId, payload)
.then(this.commitFlash);
......
......@@ -23,6 +23,7 @@ const RepoStore = {
title: '',
status: false,
},
showNewBranchDialog: false,
activeFile: Helper.getDefaultActiveFile(),
activeFileIndex: 0,
activeLine: -1,
......@@ -31,6 +32,12 @@ const RepoStore = {
isCommitable: false,
binary: false,
currentBranch: '',
startNewMR: false,
currentHash: '',
currentShortHash: '',
customBranchURL: '',
newMrTemplateUrl: '',
branchChanged: false,
commitMessage: '',
binaryTypes: {
png: false,
......@@ -49,6 +56,17 @@ const RepoStore = {
});
},
setBranchHash() {
return Service.getBranch()
.then((data) => {
if (RepoStore.currentHash !== '' && data.commit.id !== RepoStore.currentHash) {
RepoStore.branchChanged = true;
}
RepoStore.currentHash = data.commit.id;
RepoStore.currentShortHash = data.commit.short_id;
});
},
// mutations
checkIsCommitable() {
RepoStore.isCommitable = RepoStore.onTopOfBranch && RepoStore.canCommit;
......
import Cookies from 'js-cookie';
import UserTabs from './user_tabs';
export default function initUserProfile(action) {
function initUserProfile(action) {
// place profile avatars to top
$('.profile-groups-avatars').tooltip({
placement: 'top',
......@@ -17,3 +17,9 @@ export default function initUserProfile(action) {
$(this).parents('.project-limit-message').remove();
});
}
document.addEventListener('DOMContentLoaded', () => {
const page = $('body').attr('data-page');
const action = page.split(':')[1];
initUserProfile(action);
});
<script>
import Flash from '../../../flash';
import GLForm from '../../../gl_form';
import markdownHeader from './header.vue';
import markdownToolbar from './toolbar.vue';
......@@ -85,7 +86,7 @@
/*
GLForm class handles all the toolbar buttons
*/
return new gl.GLForm($(this.$refs['gl-form']), true);
return new GLForm($(this.$refs['gl-form']), true);
},
beforeDestroy() {
const glForm = $(this.$refs['gl-form']).data('gl-form');
......
......@@ -16,6 +16,11 @@ export default {
required: false,
default: 'primary',
},
closeKind: {
type: String,
required: false,
default: 'default',
},
closeButtonLabel: {
type: String,
required: false,
......@@ -33,6 +38,11 @@ export default {
[`btn-${this.kind}`]: true,
};
},
btnCancelKindClass() {
return {
[`btn-${this.closeKind}`]: true,
};
},
},
methods: {
......@@ -70,7 +80,8 @@ export default {
<div class="modal-footer">
<button
type="button"
class="btn btn-default"
class="btn"
:class="btnCancelKindClass"
@click="emitSubmit(false)">
{{closeButtonLabel}}
</button>
......
......@@ -90,7 +90,7 @@ $new-sidebar-collapsed-width: 50px;
top: $header-height;
bottom: 0;
left: 0;
background-color: $gray-normal;
background-color: $gray-light;
box-shadow: inset -2px 0 0 $border-color;
transform: translate3d(0, 0, 0);
......
......@@ -6,6 +6,7 @@
margin: 0;
list-style: none;
height: auto;
border-bottom: 1px solid $border-color;
li {
display: flex;
......@@ -24,6 +25,7 @@
&:focus {
text-decoration: none;
color: $black;
border-bottom: 2px solid $gray-darkest;
.badge {
color: $black;
......
......@@ -707,11 +707,11 @@
.frame.click-to-comment {
position: relative;
cursor: url(icon_image_comment.svg)
cursor: image-url('icon_image_comment.svg')
$image-comment-cursor-left-offset $image-comment-cursor-top-offset, auto;
// Retina cursor
cursor: -webkit-image-set(url(icon_image_comment.svg) 1x, url(icon_image_comment@2x.svg) 2x)
cursor: -webkit-image-set(image-url('icon_image_comment.svg') 1x, image-url('icon_image_comment@2x.svg') 2x)
$image-comment-cursor-left-offset $image-comment-cursor-top-offset, auto;
.comment-indicator {
......
......@@ -653,10 +653,6 @@ a.deploy-project-label {
}
.project-import {
.form-group {
margin-bottom: 0;
}
.import-btn-container {
margin-bottom: 0;
}
......
......@@ -315,20 +315,12 @@ module IssuablesHelper
@issuable_templates ||=
case issuable
when Issue
issue_template_names
ref_project.repository.issue_template_names
when MergeRequest
merge_request_template_names
ref_project.repository.merge_request_template_names
end
end
def merge_request_template_names
@merge_request_templates ||= Gitlab::Template::MergeRequestTemplate.dropdown_names(ref_project)
end
def issue_template_names
@issue_templates ||= Gitlab::Template::IssueTemplate.dropdown_names(ref_project)
end
def selected_template(issuable)
params[:issuable_template] if issuable_templates(issuable).any? { |template| template[:name] == params[:issuable_template] }
end
......
......@@ -156,7 +156,9 @@ class Blob < SimpleDelegator
end
def file_type
Gitlab::FileDetector.type_of(path)
name = File.basename(path)
Gitlab::FileDetector.type_of(path) || Gitlab::FileDetector.type_of(name)
end
def video?
......
class OauthAccessToken < Doorkeeper::AccessToken
belongs_to :resource_owner, class_name: 'User'
belongs_to :application, class_name: 'Doorkeeper::Application'
alias_method :user, :resource_owner
end
......@@ -41,7 +41,8 @@ class Repository
CACHED_METHODS = %i(size commit_count rendered_readme contribution_guide
changelog license_blob license_key gitignore koding_yml
gitlab_ci_yml branch_names tag_names branch_count
tag_count avatar exists? empty? root_ref has_visible_content?).freeze
tag_count avatar exists? empty? root_ref has_visible_content?
issue_template_names merge_request_template_names).freeze
# Methods that use cache_method but only memoize the value
MEMOIZED_CACHED_METHODS = %i(license empty_repo?).freeze
......@@ -57,7 +58,9 @@ class Repository
gitignore: :gitignore,
koding: :koding_yml,
gitlab_ci: :gitlab_ci_yml,
avatar: :avatar
avatar: :avatar,
issue_template: :issue_template_names,
merge_request_template: :merge_request_template_names
}.freeze
# Wraps around the given method and caches its output in Redis and an instance
......@@ -540,6 +543,16 @@ class Repository
end
cache_method :avatar
def issue_template_names
Gitlab::Template::IssueTemplate.dropdown_names(project)
end
cache_method :issue_template_names, fallback: []
def merge_request_template_names
Gitlab::Template::MergeRequestTemplate.dropdown_names(project)
end
cache_method :merge_request_template_names, fallback: []
def readme
if readme = tree(:head)&.readme
ReadmeBlob.new(readme, self)
......
......@@ -56,11 +56,22 @@ module Auth
def process_scope(scope)
type, name, actions = scope.split(':', 3)
actions = actions.split(',')
path = ContainerRegistry::Path.new(name)
return unless type == 'repository'
case type
when 'registry'
process_registry_access(type, name, actions)
when 'repository'
path = ContainerRegistry::Path.new(name)
process_repository_access(type, path, actions)
end
end
def process_registry_access(type, name, actions)
return unless current_user&.admin?
return unless name == 'catalog'
return unless actions == ['*']
process_repository_access(type, path, actions)
{ type: type, name: name, actions: ['*'] }
end
def process_repository_access(type, path, actions)
......
......@@ -84,13 +84,9 @@ module MergeRequests
def after_merge
MergeRequests::PostMergeService.new(project, current_user).execute(merge_request)
if params[:should_remove_source_branch].present? || @merge_request.force_remove_source_branch?
# Verify again that the source branch can be removed, since branch may be protected,
# or the source branch may have been updated.
if @merge_request.can_remove_source_branch?(branch_deletion_user)
DeleteBranchService.new(@merge_request.source_project, branch_deletion_user)
.execute(merge_request.source_branch)
end
if delete_source_branch?
DeleteBranchService.new(@merge_request.source_project, branch_deletion_user)
.execute(merge_request.source_branch)
end
end
......@@ -102,6 +98,14 @@ module MergeRequests
@merge_request.force_remove_source_branch? ? @merge_request.author : current_user
end
# Verify again that the source branch can be removed, since branch may be protected,
# or the source branch may have been updated, or the user may not have permission
#
def delete_source_branch?
params.fetch('should_remove_source_branch', @merge_request.force_remove_source_branch?) &&
@merge_request.can_remove_source_branch?(branch_deletion_user)
end
# Logs merge error message and cleans `MergeRequest#merge_jid`.
#
def handle_merge_error(log_message:, save_message_on_model: false)
......
......@@ -413,7 +413,7 @@ class NotificationService
end
def relabeled_resource_email(target, labels, current_user, method)
recipients = labels.flat_map { |l| l.subscribers(target.project) }
recipients = labels.flat_map { |l| l.subscribers(target.project) }.uniq
recipients = notifiable_users(
recipients, :subscription,
target: target,
......
......@@ -31,7 +31,7 @@
%tbody
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
%img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-branch-gray.gif'), style: "display:block;", width: "13", alt: "Branch icon" }/
%img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-branch-gray.gif'), style: "display:block;", width: "13", alt: "" }/
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
%a.muted{ href: commits_url(@pipeline), style: "color:#333333;text-decoration:none;" }
= @pipeline.ref
......@@ -42,7 +42,7 @@
%tbody
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
%img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-commit-gray.gif'), style: "display:block;", width: "13", alt: "Commit icon" }/
%img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-commit-gray.gif'), style: "display:block;", width: "13", alt: "" }/
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
%a{ href: commit_url(@pipeline), style: "color:#3777b0;text-decoration:none;" }
= @pipeline.short_sha
......@@ -60,7 +60,7 @@
%tbody
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
%img.avatar{ height: "24", src: avatar_icon(commit.author || commit.author_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/
%img.avatar{ height: "24", src: avatar_icon(commit.author || commit.author_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "" }/
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
- if commit.author
%a.muted{ href: user_url(commit.author), style: "color:#333333;text-decoration:none;" }
......@@ -76,7 +76,7 @@
%tbody
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
%img.avatar{ height: "24", src: avatar_icon(commit.committer || commit.committer_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/
%img.avatar{ height: "24", src: avatar_icon(commit.committer || commit.committer_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "" }/
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
- if commit.committer
%a.muted{ href: user_url(commit.committer), style: "color:#333333;text-decoration:none;" }
......@@ -100,7 +100,7 @@
triggered by
- if @pipeline.user
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;padding-left:5px", width: "24" }
%img.avatar{ height: "24", src: avatar_icon(@pipeline.user, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/
%img.avatar{ height: "24", src: avatar_icon(@pipeline.user, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "" }/
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;font-weight:500;line-height:1.4;vertical-align:baseline;" }
%a.muted{ href: user_url(@pipeline.user), style: "color:#333333;text-decoration:none;" }
= @pipeline.user.name
......
......@@ -31,7 +31,7 @@
%tbody
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
%img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-branch-gray.gif'), style: "display:block;", width: "13", alt: "Branch icon" }/
%img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-branch-gray.gif'), style: "display:block;", width: "13", alt: "" }/
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
%a.muted{ href: commits_url(@pipeline), style: "color:#333333;text-decoration:none;" }
= @pipeline.ref
......@@ -42,7 +42,7 @@
%tbody
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
%img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-commit-gray.gif'), style: "display:block;", width: "13", alt: "Commit icon" }/
%img{ height: "13", src: image_url('mailers/ci_pipeline_notif_v1/icon-commit-gray.gif'), style: "display:block;", width: "13", alt: "" }/
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
%a{ href: commit_url(@pipeline), style: "color:#3777b0;text-decoration:none;" }
= @pipeline.short_sha
......@@ -60,7 +60,7 @@
%tbody
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
%img.avatar{ height: "24", src: avatar_icon(commit.author || commit.author_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/
%img.avatar{ height: "24", src: avatar_icon(commit.author || commit.author_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "" }/
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
- if commit.author
%a.muted{ href: user_url(commit.author), style: "color:#333333;text-decoration:none;" }
......@@ -76,7 +76,7 @@
%tbody
%tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
%img.avatar{ height: "24", src: avatar_icon(commit.committer || commit.committer_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/
%img.avatar{ height: "24", src: avatar_icon(commit.committer || commit.committer_email, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "" }/
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
- if commit.committer
%a.muted{ href: user_url(commit.committer), style: "color:#333333;text-decoration:none;" }
......@@ -100,7 +100,7 @@
triggered by
- if @pipeline.user
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;padding-left:5px", width: "24" }
%img.avatar{ height: "24", src: avatar_icon(@pipeline.user, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/
%img.avatar{ height: "24", src: avatar_icon(@pipeline.user, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "" }/
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;font-weight:500;line-height:1.4;vertical-align:baseline;" }
%a.muted{ href: user_url(@pipeline.user), style: "color:#333333;text-decoration:none;" }
= @pipeline.user.name
......
......@@ -3,5 +3,7 @@
refs_url: refs_project_path(project, format: :json),
project_url: project_path(project),
project_id: project.id,
blob_url: namespace_project_blob_path(project.namespace, project, '{{branch}}'),
new_mr_template_url: namespace_project_new_merge_request_path(project.namespace, project, merge_request: { source_branch: '{{source_branch}}' }),
can_commit: (!!can_push_branch?(project, @ref)).to_s,
on_top_of_branch: (!!on_top_of_branch?(project, @ref)).to_s } }
......@@ -4,6 +4,9 @@
- page_description @user.bio
- header_title @user.name, user_path(@user)
- @no_container = true
- content_for :page_specific_javascripts do
= webpack_bundle_tag 'common_d3'
= webpack_bundle_tag 'users'
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, user_url(@user, format: :atom), title: "#{@user.name} activity")
......
---
title: Issue JWT token with registry:catalog:* scope when requested by GitLab admin
merge_request: 14751
author: Vratislav Kalenda
type: added
---
title: Fixed 'Removed source branch' checkbox in merge widget being ignored.
merge_request: 14832
author:
type: fixed
---
title: Fix flash errors showing up on a non configured prometheus integration
merge_request: 35652
author:
type: fixed
---
title: Decreases z-index of select2 to a lower number of our navigation bar
merge_request:
author:
type: fixed
---
title: Change background color of nav sidebar to match other gl sidebars
merge_request:
author:
type: changed
---
title: Fixed duplicate notifications when added multiple labels on an issue
merge_request: 14798
author:
type: fixed
---
title: Removed d3.js from the graph and users bundles and used the common_d3 bundle
instead
merge_request: 14826
author:
type: other
---
title: Cache issue and MR template names in Redis
merge_request:
author:
type: other
---
title: Fix unnecessary ajax requests in admin broadcast message form
merge_request: 14853
author:
type: fixed
---
title: Remove unnecessary alt-texts from pipeline emails
merge_request: 14602
author: gernberg
type: fixed
---
title: 'Repo Editor: Add option to start a new MR directly from comit section'
merge_request: 14665
author:
type: added
---
title: Replace the 'features/explore/projects.feature' spinach test with an rspec analog
merge_request: 14755
author: Vitaliy @blackst0ne Klachkov
type: other
......@@ -93,6 +93,7 @@ var config = {
vue_merge_request_widget: './vue_merge_request_widget/index.js',
test: './test.js',
two_factor_auth: './two_factor_auth.js',
users: './users/index.js',
performance_bar: './performance_bar.js',
webpack_runtime: './webpack.js',
},
......@@ -226,8 +227,10 @@ var config = {
name: 'common_d3',
chunks: [
'graphs',
'graphs_show',
'monitoring',
'burndown_chart',
'users',
'burndown_chart', // EE
],
}),
......
......@@ -58,6 +58,18 @@ For example, in French we translate `you` as the informal `tu`.
You can refer to other translated strings and notes in the glossary to assist determining a
suitable level of formality.
### Inclusive language
[Diversity] is one of GitLab's values.
We ask you to avoid translations which exclude people based on their gender or ethnicity.
In languages which distinguish between a male and female form,
use both or choose a neutral formulation.
For example in German, the word "user" can be translated into "Benutzer" (male) or "Benutzerin" (female).
Therefore "create a new user" would translate into "Benutzer(in) anlegen".
[Diversity]: https://about.gitlab.com/handbook/values/#diversity
### Updating the glossary
To propose additions to the glossary please
......
......@@ -27,3 +27,13 @@ Bullet will log query problems to both the Rails log as well as the Chrome
console.
As a follow up to finding `N+1` queries with Bullet, consider writing a [QueryRecoder test](query_recorder.md) to prevent a regression.
## GitLab Profiler
[Gitlab-Profiler](https://gitlab.com/gitlab-com/gitlab-profiler) was built to
help developers understand why specific URLs of their application may be slow
and to provide hard data that can help reduce load times.
For GitLab.com, you can find the latest results here:
<http://redash.gitlab.com/dashboard/gitlab-profiler-statistics>
......@@ -121,7 +121,7 @@ The use of Ruby version managers such as [RVM], [rbenv] or [chruby] with GitLab
in production, frequently leads to hard to diagnose problems. For example,
GitLab Shell is called from OpenSSH, and having a version manager can prevent
pushing and pulling over SSH. Version managers are not supported and we strongly
advise everyone to follow the instructions below to use a system Ruby.
advise everyone to follow the instructions below to use a system Ruby.
Linux distributions generally have older versions of Ruby available, so these
instructions are designed to install Ruby from the official source code.
......@@ -299,9 +299,9 @@ sudo usermod -aG redis git
### Clone the Source
# Clone GitLab repository
sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 10-0-stable gitlab
sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 10-1-stable gitlab
**Note:** You can change `10-0-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
**Note:** You can change `10-1-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
### Configure It
......
......@@ -150,7 +150,7 @@ sudo -u git -H make
#### New Gitaly configuration options required
In order to function Gitaly needs some additional configuration information. Below we assume you installed Gitaly in `/home/git/gitaly` and GitLab Shell in `/home/git/gitlab-shell'.
In order to function Gitaly needs some additional configuration information. Below we assume you installed Gitaly in `/home/git/gitaly` and GitLab Shell in `/home/git/gitlab-shell`.
```shell
echo '
......@@ -335,11 +335,11 @@ sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production
If all items are green, then congratulations, the upgrade is complete!
## Things went south? Revert to previous version (9.5)
## Things went south? Revert to previous version (10.0)
### 1. Revert the code to the previous version
Follow the [upgrade guide from 9.4 to 9.5](9.4-to-9.5.md), except for the
Follow the [upgrade guide from 9.5 to 10.0](9.5-to-10.0.md), except for the
database migration (the backup is already migrated to the previous version).
### 2. Restore from the backup
......
......@@ -155,7 +155,7 @@ comments in greater detail.
## Image discussions
> [Introduced][ce-14531] in GitLab 10.1.
> [Introduced][ce-14061] in GitLab 10.1.
Sometimes a discussion is revolved around an image. With image discussions,
you can easily target a specific coordinate of an image and start a discussion
......@@ -227,6 +227,7 @@ edit existing comments. Non-team members are restricted from adding or editing c
[ce-7180]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7180
[ce-8266]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8266
[ce-14053]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14053
[ce-14061]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14061
[ce-14531]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14531
[resolve-discussion-button]: img/resolve_discussion_button.png
[resolve-comment-button]: img/resolve_comment_button.png
......
@public
Feature: Explore Projects
Background:
Given public project "Community"
And internal project "Internal"
And private project "Enterprise"
Scenario: I visit public area
Given archived project "Archive"
When I visit the public projects area
Then I should see project "Community"
And I should not see project "Internal"
And I should not see project "Enterprise"
And I should not see project "Archive"
Scenario: I visit public project page
When I visit project "Community" page
Then I should see project "Community" home page
Scenario: I visit internal project page
When I visit project "Internal" page
Then I should be redirected to sign in page
Scenario: I visit private project page
When I visit project "Enterprise" page
Then I should be redirected to sign in page
Scenario: I visit an empty public project page
Given public empty project "Empty Public Project"
When I visit empty project page
Then I should see empty public project details
And I should see empty public project details with http clone info
Scenario: I visit an empty public project page as user with no ssh-keys
Given I sign in as a user
And I have no ssh keys
And public empty project "Empty Public Project"
When I visit empty project page
Then I should see empty public project details
And I should see empty public project details with http clone info
Scenario: I visit an empty public project page as user with an ssh-key
Given I sign in as a user
And I have an ssh key
And public empty project "Empty Public Project"
When I visit empty project page
Then I should see empty public project details
And I should see empty public project details with ssh clone info
Scenario: I visit public area as user
Given archived project "Archive"
And I sign in as a user
When I visit the public projects area
Then I should see project "Community"
And I should see project "Internal"
And I should not see project "Enterprise"
And I should not see project "Archive"
Scenario: I visit internal project page as user
Given I sign in as a user
When I visit project "Internal" page
Then I should see project "Internal" home page
Scenario: I visit public project page
When I visit project "Community" page
Then I should see project "Community" home page
And I should see an http link to the repository
Scenario: I visit public project page as user with no ssh-keys
Given I sign in as a user
And I have no ssh keys
When I visit project "Community" page
Then I should see project "Community" home page
And I should see an http link to the repository
Scenario: I visit public project page as user with an ssh-key
Given I sign in as a user
And I have an ssh key
When I visit project "Community" page
Then I should see project "Community" home page
And I should see an ssh link to the repository
Scenario: I visit an empty public project page
Given public empty project "Empty Public Project"
When I visit empty project page
Then I should see empty public project details
Scenario: I visit public project issues page as a non authorized user
Given I visit project "Community" page
Then I should not see command line instructions
And I visit "Community" issues page
Then I should see list of issues for "Community" project
Scenario: I visit public project issues page as authorized user
Given I sign in as a user
Given I visit project "Community" page
And I visit "Community" issues page
Then I should see list of issues for "Community" project
Scenario: I visit internal project issues page as authorized user
Given I sign in as a user
Given I visit project "Internal" page
And I visit "Internal" issues page
Then I should see list of issues for "Internal" project
Scenario: I visit public project merge requests page as an authorized user
Given I sign in as a user
Given I visit project "Community" page
And I visit "Community" merge requests page
And project "Community" has "Bug fix" open merge request
Then I should see list of merge requests for "Community" project
Scenario: I visit public project merge requests page as a non authorized user
Given I visit project "Community" page
And I visit "Community" merge requests page
And project "Community" has "Bug fix" open merge request
Then I should see list of merge requests for "Community" project
Scenario: I visit internal project merge requests page as an authorized user
Given I sign in as a user
Given I visit project "Internal" page
And I visit "Internal" merge requests page
And project "Internal" has "Feature implemented" open merge request
Then I should see list of merge requests for "Internal" project
Scenario: Trending page
Given archived project "Archive"
And project "Archive" has comments
And I sign in as a user
And project "Community" has comments
And trending projects are refreshed
When I visit the explore trending projects
Then I should see project "Community"
And I should not see project "Internal"
And I should not see project "Enterprise"
And I should not see project "Archive"
Scenario: Most starred page
Given archived project "Archive"
And I sign in as a user
When I visit the explore starred projects
Then I should see project "Community"
And I should see project "Internal"
And I should not see project "Archive"
class Spinach::Features::ExploreProjects < Spinach::FeatureSteps
include SharedAuthentication
include SharedPaths
include SharedProject
include SharedUser
step 'I should see project "Empty Public Project"' do
expect(page).to have_content "Empty Public Project"
end
step 'I should see public project details' do
expect(page).to have_content '32 branches'
expect(page).to have_content '16 tags'
end
step 'I should see project readme' do
expect(page).to have_content 'README.md'
end
step 'I should see empty public project details' do
expect(page).not_to have_content 'Git global setup'
end
step 'I should see empty public project details with http clone info' do
project = Project.find_by(name: 'Empty Public Project')
page.all(:css, '.git-empty .clone').each do |element|
expect(element.text).to include(project.http_url_to_repo)
end
end
step 'I should see empty public project details with ssh clone info' do
project = Project.find_by(name: 'Empty Public Project')
page.all(:css, '.git-empty .clone').each do |element|
expect(element.text).to include(project.url_to_repo)
end
end
step 'I should see project "Community" home page' do
page.within '.breadcrumbs .breadcrumb-item-text' do
expect(page).to have_content 'Community'
end
end
step 'I should see project "Internal" home page' do
page.within '.breadcrumbs .breadcrumb-item-text' do
expect(page).to have_content 'Internal'
end
end
step 'I should see an http link to the repository' do
project = Project.find_by(name: 'Community')
expect(page).to have_field('project_clone', with: project.http_url_to_repo)
end
step 'I should see an ssh link to the repository' do
project = Project.find_by(name: 'Community')
expect(page).to have_field('project_clone', with: project.url_to_repo)
end
step 'I visit "Community" issues page' do
create(:issue,
title: "Bug",
project: public_project
)
create(:issue,
title: "New feature",
project: public_project
)
visit project_issues_path(public_project)
end
step 'I should see list of issues for "Community" project' do
expect(page).to have_content "Bug"
expect(page).to have_content public_project.name
expect(page).to have_content "New feature"
end
step 'I visit "Internal" issues page' do
create(:issue,
title: "Internal Bug",
project: internal_project
)
create(:issue,
title: "New internal feature",
project: internal_project
)
visit project_issues_path(internal_project)
end
step 'I should see list of issues for "Internal" project' do
expect(page).to have_content "Internal Bug"
expect(page).to have_content internal_project.name
expect(page).to have_content "New internal feature"
end
step 'I visit "Community" merge requests page' do
visit project_merge_requests_path(public_project)
end
step 'project "Community" has "Bug fix" open merge request' do
create(:merge_request,
title: "Bug fix for public project",
source_project: public_project,
target_project: public_project
)
end
step 'I should see list of merge requests for "Community" project' do
expect(page).to have_content public_project.name
expect(page).to have_content public_merge_request.source_project.name
end
step 'I visit "Internal" merge requests page' do
visit project_merge_requests_path(internal_project)
end
step 'project "Internal" has "Feature implemented" open merge request' do
create(:merge_request,
title: "Feature implemented",
source_project: internal_project,
target_project: internal_project
)
end
step 'I should see list of merge requests for "Internal" project' do
expect(page).to have_content internal_project.name
expect(page).to have_content internal_merge_request.source_project.name
end
def internal_project
@internal_project ||= Project.find_by!(name: 'Internal')
end
def public_project
@public_project ||= Project.find_by!(name: 'Community')
end
def internal_merge_request
@internal_merge_request ||= MergeRequest.find_by!(title: 'Feature implemented')
end
def public_merge_request
@public_merge_request ||= MergeRequest.find_by!(title: 'Bug fix for public project')
end
end
......@@ -478,19 +478,6 @@ module SharedPaths
# ----------------------------------------
# Public Projects
# ----------------------------------------
step 'I visit the public projects area' do
visit explore_projects_path
end
step 'I visit the explore trending projects' do
visit trending_explore_projects_path
end
step 'I visit the explore starred projects' do
visit starred_explore_projects_path
end
step 'I visit the public groups area' do
visit explore_groups_path
end
......
......@@ -112,10 +112,6 @@ module SharedProject
# Visibility of archived project
# ----------------------------------------
step 'archived project "Archive"' do
create(:project, :archived, :public, :repository, name: 'Archive')
end
step 'I should not see project "Archive"' do
project = Project.find_by(name: "Archive")
expect(page).not_to have_content project.name_with_namespace
......@@ -126,11 +122,6 @@ module SharedProject
expect(page).to have_content project.name_with_namespace
end
step 'project "Archive" has comments' do
project = Project.find_by(name: "Archive")
2.times { create(:note_on_issue, project: project) }
end
# ----------------------------------------
# Visibility level
# ----------------------------------------
......@@ -209,15 +200,6 @@ module SharedProject
create :project_empty_repo, :public, name: "Empty Public Project"
end
step 'project "Community" has comments' do
project = Project.find_by(name: "Community")
2.times { create(:note_on_issue, project: project) }
end
step 'trending projects are refreshed' do
TrendingProject.refresh!
end
step 'project "Shop" has labels: "bug", "feature", "enhancement"' do
project = Project.find_by(name: "Shop")
create(:label, project: project, title: 'bug')
......
......@@ -44,6 +44,39 @@ module API
# Helper Methods for Grape Endpoint
module HelperMethods
def find_current_user
user =
find_user_from_private_token ||
find_user_from_oauth_token ||
find_user_from_warden ||
find_user_by_job_token
return nil unless user
raise UnauthorizedError unless Gitlab::UserAccess.new(user).allowed? && user.can?(:access_api)
user
end
def private_token
params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER]
end
private
def find_user_from_private_token
token_string = private_token.to_s
return nil unless token_string.present?
user =
find_user_by_authentication_token(token_string) ||
find_user_by_personal_access_token(token_string)
raise UnauthorizedError unless user
user
end
# Invokes the doorkeeper guard.
#
# If token is presented and valid, then it sets @current_user.
......@@ -62,37 +95,36 @@ module API
# scopes: (optional) scopes required for this guard.
# Defaults to empty array.
#
def doorkeeper_guard(scopes: [])
access_token = find_access_token
return nil unless access_token
case AccessTokenValidationService.new(access_token, request: request).validate(scopes: scopes)
when AccessTokenValidationService::INSUFFICIENT_SCOPE
raise InsufficientScopeError.new(scopes)
when AccessTokenValidationService::EXPIRED
raise ExpiredError
def find_user_from_oauth_token
access_token = find_oauth_access_token
return unless access_token
when AccessTokenValidationService::REVOKED
raise RevokedError
find_user_by_access_token(access_token)
end
when AccessTokenValidationService::VALID
User.find(access_token.resource_owner_id)
end
def find_user_by_authentication_token(token_string)
User.find_by_authentication_token(token_string)
end
def find_user_by_private_token(scopes: [])
token_string = (params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER]).to_s
def find_user_by_personal_access_token(token_string)
access_token = PersonalAccessToken.find_by_token(token_string)
return unless access_token
return nil unless token_string.present?
find_user_by_access_token(access_token)
end
user =
find_user_by_authentication_token(token_string) ||
find_user_by_personal_access_token(token_string, scopes)
# Check the Rails session for valid authentication details
def find_user_from_warden
warden.try(:authenticate) if verified_request?
end
raise UnauthorizedError unless user
def warden
env['warden']
end
user
# Check if the request is GET/HEAD, or if CSRF token is valid.
def verified_request?
Gitlab::RequestForgeryProtection.verified?(env)
end
def find_user_by_job_token
......@@ -105,43 +137,63 @@ module API
end
end
private
def route_authentication_setting
return {} unless respond_to?(:route_setting)
route_setting(:authentication) || {}
end
def find_user_by_authentication_token(token_string)
User.find_by_authentication_token(token_string)
end
def find_oauth_access_token
return @oauth_access_token if defined?(@oauth_access_token)
def find_user_by_personal_access_token(token_string, scopes)
access_token = PersonalAccessToken.active.find_by_token(token_string)
return unless access_token
token = Doorkeeper::OAuth::Token.from_request(doorkeeper_request, *Doorkeeper.configuration.access_token_methods)
return @oauth_access_token = nil unless token
if AccessTokenValidationService.new(access_token, request: request).include_any_scope?(scopes)
User.find(access_token.user_id)
end
@oauth_access_token = OauthAccessToken.by_token(token)
raise UnauthorizedError unless @oauth_access_token
@oauth_access_token.revoke_previous_refresh_token!
@oauth_access_token
end
def find_access_token
return @access_token if defined?(@access_token)
def find_user_by_access_token(access_token)
scopes = scopes_registered_for_endpoint
token = Doorkeeper::OAuth::Token.from_request(doorkeeper_request, *Doorkeeper.configuration.access_token_methods)
return @access_token = nil unless token
case AccessTokenValidationService.new(access_token, request: request).validate(scopes: scopes)
when AccessTokenValidationService::INSUFFICIENT_SCOPE
raise InsufficientScopeError.new(scopes)
@access_token = Doorkeeper::AccessToken.by_token(token)
raise UnauthorizedError unless @access_token
when AccessTokenValidationService::EXPIRED
raise ExpiredError
@access_token.revoke_previous_refresh_token!
@access_token
when AccessTokenValidationService::REVOKED
raise RevokedError
when AccessTokenValidationService::VALID
access_token.user
end
end
def doorkeeper_request
@doorkeeper_request ||= ActionDispatch::Request.new(env)
end
# An array of scopes that were registered (using `allow_access_with_scope`)
# for the current endpoint class. It also returns scopes registered on
# `API::API`, since these are meant to apply to all API routes.
def scopes_registered_for_endpoint
@scopes_registered_for_endpoint ||=
begin
endpoint_classes = [options[:for].presence, ::API::API].compact
endpoint_classes.reduce([]) do |memo, endpoint|
if endpoint.respond_to?(:allowed_scopes)
memo.concat(endpoint.allowed_scopes)
else
memo
end
end
end
end
end
module ClassMethods
......
......@@ -5,8 +5,6 @@ module API
include Gitlab::Utils
include Helpers::Pagination
UnauthorizedError = Class.new(StandardError)
SUDO_HEADER = "HTTP_SUDO".freeze
SUDO_PARAM = :sudo
......@@ -426,25 +424,11 @@ module API
begin
@initial_current_user = Gitlab::Auth::UniqueIpsLimiter.limit_user! { find_current_user }
rescue APIGuard::UnauthorizedError, UnauthorizedError
rescue APIGuard::UnauthorizedError
unauthorized!
end
end
def find_current_user
user =
find_user_by_private_token(scopes: scopes_registered_for_endpoint) ||
doorkeeper_guard(scopes: scopes_registered_for_endpoint) ||
find_user_from_warden ||
find_user_by_job_token
return nil unless user
raise UnauthorizedError unless Gitlab::UserAccess.new(user).allowed? && user.can?(:access_api)
user
end
def sudo!
return unless sudo_identifier
return unless initial_current_user
......@@ -508,22 +492,5 @@ module API
exception.status == 500
end
# An array of scopes that were registered (using `allow_access_with_scope`)
# for the current endpoint class. It also returns scopes registered on
# `API::API`, since these are meant to apply to all API routes.
def scopes_registered_for_endpoint
@scopes_registered_for_endpoint ||=
begin
endpoint_classes = [options[:for].presence, ::API::API].compact
endpoint_classes.reduce([]) do |memo, endpoint|
if endpoint.respond_to?(:allowed_scopes)
memo.concat(endpoint.allowed_scopes)
else
memo
end
end
end
end
end
end
......@@ -6,31 +6,33 @@ module Gitlab
module FileDetector
PATTERNS = {
# Project files
readme: /\Areadme/i,
changelog: /\A(changelog|history|changes|news)/i,
license: /\A(licen[sc]e|copying)(\..+|\z)/i,
contributing: /\Acontributing/i,
readme: /\Areadme[^\/]*\z/i,
changelog: /\A(changelog|history|changes|news)[^\/]*\z/i,
license: /\A(licen[sc]e|copying)(\.[^\/]+)?\z/i,
contributing: /\Acontributing[^\/]*\z/i,
version: 'version',
avatar: /\Alogo\.(png|jpg|gif)\z/,
issue_template: /\A\.gitlab\/issue_templates\/[^\/]+\.md\z/,
merge_request_template: /\A\.gitlab\/merge_request_templates\/[^\/]+\.md\z/,
# Configuration files
gitignore: '.gitignore',
koding: '.koding.yml',
gitlab_ci: '.gitlab-ci.yml',
route_map: 'route-map.yml',
route_map: '.gitlab/route-map.yml',
# Dependency files
cartfile: /\ACartfile/,
cartfile: /\ACartfile[^\/]*\z/,
composer_json: 'composer.json',
gemfile: /\A(Gemfile|gems\.rb)\z/,
gemfile_lock: 'Gemfile.lock',
gemspec: /\.gemspec\z/,
gemspec: /\A[^\/]*\.gemspec\z/,
godeps_json: 'Godeps.json',
package_json: 'package.json',
podfile: 'Podfile',
podspec_json: /\.podspec\.json\z/,
podspec: /\.podspec\z/,
requirements_txt: /requirements\.txt\z/,
podspec_json: /\A[^\/]*\.podspec\.json\z/,
podspec: /\A[^\/]*\.podspec\z/,
requirements_txt: /\A[^\/]*requirements\.txt\z/,
yarn_lock: 'yarn.lock'
}.freeze
......@@ -63,13 +65,11 @@ module Gitlab
# type_of('README.md') # => :readme
# type_of('VERSION') # => :version
def self.type_of(path)
name = File.basename(path)
PATTERNS.each do |type, search|
did_match = if search.is_a?(Regexp)
name =~ search
path =~ search
else
name.casecmp(search) == 0
path.casecmp(search) == 0
end
return type if did_match
......
......@@ -79,6 +79,12 @@ FactoryGirl.define do
merge_user author
end
trait :remove_source_branch do
merge_params do
{ 'force_remove_source_branch' => '1' }
end
end
after(:build) do |merge_request|
target_project = merge_request.target_project
source_project = merge_request.source_project
......
require 'spec_helper'
describe 'User explores projects' do
set(:archived_project) { create(:project, :archived) }
set(:internal_project) { create(:project, :internal) }
set(:private_project) { create(:project, :private) }
set(:public_project) { create(:project, :public) }
shared_examples_for 'shows public projects' do
it 'shows projects' do
expect(page).to have_content(public_project.title)
expect(page).not_to have_content(internal_project.title)
expect(page).not_to have_content(private_project.title)
expect(page).not_to have_content(archived_project.title)
end
end
shared_examples_for 'shows public and internal projects' do
it 'shows projects' do
expect(page).to have_content(public_project.title)
expect(page).to have_content(internal_project.title)
expect(page).not_to have_content(private_project.title)
expect(page).not_to have_content(archived_project.title)
end
end
context 'when not signed in' do
context 'when viewing public projects' do
before do
visit(explore_projects_path)
end
include_examples 'shows public projects'
end
end
context 'when signed in' do
set(:user) { create(:user) }
before do
sign_in(user)
end
context 'when viewing public projects' do
before do
visit(explore_projects_path)
end
include_examples 'shows public and internal projects'
end
context 'when viewing most starred projects' do
before do
visit(starred_explore_projects_path)
end
include_examples 'shows public and internal projects'
end
context 'when viewing trending projects' do
before do
[archived_project, public_project].each { |project| create(:note_on_issue, project: project) }
TrendingProject.refresh!
visit(trending_explore_projects_path)
end
include_examples 'shows public projects'
end
end
end
require 'spec_helper'
feature 'Issues List' do
let(:user) { create(:user) }
let(:project) { create(:project) }
background do
project.team << [user, :developer]
sign_in(user)
end
scenario 'user does not see create new list button' do
create(:issue, project: project)
visit project_issues_path(project)
expect(page).not_to have_selector('.js-new-board-list')
end
end
require 'spec_helper'
describe 'User views issues' do
set(:user) { create(:user) }
shared_examples_for 'shows issues' do
it 'shows issues' do
expect(page).to have_content(project.name)
.and have_content(issue1.title)
.and have_content(issue2.title)
.and have_no_selector('.js-new-board-list')
end
end
context 'when project is public' do
set(:project) { create(:project_empty_repo, :public) }
set(:issue1) { create(:issue, project: project) }
set(:issue2) { create(:issue, project: project) }
context 'when signed in' do
before do
project.add_developer(user)
sign_in(user)
visit(project_issues_path(project))
end
include_examples 'shows issues'
end
context 'when not signed in' do
before do
visit(project_issues_path(project))
end
include_examples 'shows issues'
end
end
context 'when project is internal' do
set(:project) { create(:project_empty_repo, :internal) }
set(:issue1) { create(:issue, project: project) }
set(:issue2) { create(:issue, project: project) }
context 'when signed in' do
before do
project.add_developer(user)
sign_in(user)
visit(project_issues_path(project))
end
include_examples 'shows issues'
end
end
end
require 'spec_helper'
describe 'User views open merge requests' do
let(:project) { create(:project, :public, :repository) }
set(:user) { create(:user) }
context "when the target branch is the project's default branch" do
let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
let!(:closed_merge_request) { create(:closed_merge_request, source_project: project, target_project: project) }
before do
visit(project_merge_requests_path(project))
shared_examples_for 'shows merge requests' do
it 'shows merge requests' do
expect(page).to have_content(project.name).and have_content(merge_request.source_project.name)
end
end
it 'shows open merge requests' do
expect(page).to have_content(merge_request.title).and have_no_content(closed_merge_request.title)
end
context 'when project is public' do
set(:project) { create(:project, :public, :repository) }
it 'does not show target branch name' do
expect(page).to have_content(merge_request.title)
expect(find('.issuable-info')).not_to have_content(project.default_branch)
end
end
context 'when not signed in' do
context "when the target branch is the project's default branch" do
let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
let!(:closed_merge_request) { create(:closed_merge_request, source_project: project, target_project: project) }
context "when the target branch is different from the project's default branch" do
let!(:merge_request) do
create(:merge_request,
source_project: project,
target_project: project,
source_branch: 'fix',
target_branch: 'feature_conflict')
end
before do
visit(project_merge_requests_path(project))
end
before do
visit(project_merge_requests_path(project))
end
include_examples 'shows merge requests'
it 'shows target branch name' do
expect(page).to have_content(merge_request.target_branch)
end
end
it 'shows open merge requests' do
expect(page).to have_content(merge_request.title).and have_no_content(closed_merge_request.title)
end
context 'when a merge request has pipelines' do
let!(:build) { create :ci_build, pipeline: pipeline }
it 'does not show target branch name' do
expect(page).to have_content(merge_request.title)
expect(find('.issuable-info')).not_to have_content(project.default_branch)
end
end
let(:merge_request) do
create(:merge_request_with_diffs,
source_project: project,
target_project: project,
source_branch: 'merge-test')
end
context "when the target branch is different from the project's default branch" do
let!(:merge_request) do
create(:merge_request,
source_project: project,
target_project: project,
source_branch: 'fix',
target_branch: 'feature_conflict')
end
before do
visit(project_merge_requests_path(project))
end
it 'shows target branch name' do
expect(page).to have_content(merge_request.target_branch)
end
end
let(:pipeline) do
create(:ci_pipeline,
project: project,
sha: merge_request.diff_head_sha,
ref: merge_request.source_branch,
head_pipeline_of: merge_request)
context 'when a merge request has pipelines' do
let!(:build) { create :ci_build, pipeline: pipeline }
let(:merge_request) do
create(:merge_request_with_diffs,
source_project: project,
target_project: project,
source_branch: 'merge-test')
end
let(:pipeline) do
create(:ci_pipeline,
project: project,
sha: merge_request.diff_head_sha,
ref: merge_request.source_branch,
head_pipeline_of: merge_request)
end
before do
project.enable_ci
visit(project_merge_requests_path(project))
end
it 'shows pipeline status' do
page.within('.mr-list') do
expect(page).to have_link('Pipeline: pending')
end
end
end
end
before do
project.enable_ci
context 'when signed in' do
let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
before do
project.add_developer(user)
sign_in(user)
visit(project_merge_requests_path(project))
visit(project_merge_requests_path(project))
end
include_examples 'shows merge requests'
end
end
it 'shows pipeline status' do
page.within('.mr-list') do
expect(page).to have_link('Pipeline: pending')
context 'when project is internal' do
let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
set(:project) { create(:project, :internal, :repository) }
context 'when signed in' do
before do
project.add_developer(user)
sign_in(user)
visit(project_merge_requests_path(project))
end
include_examples 'shows merge requests'
end
end
end
require 'spec_helper'
describe 'User views details' do
set(:user) { create(:user) }
shared_examples_for 'redirects to the sign in page' do
it 'redirects to the sign in page' do
expect(current_path).to eq(new_user_session_path)
end
end
shared_examples_for 'shows details of empty project' do
let(:user_has_ssh_key) { false }
it 'shows details' do
expect(page).not_to have_content('Git global setup')
page.all(:css, '.git-empty .clone').each do |element|
expect(element.text).to include(project.http_url_to_repo)
end
expect(page).to have_field('project_clone', with: project.http_url_to_repo) unless user_has_ssh_key
end
end
shared_examples_for 'shows details of non empty project' do
let(:user_has_ssh_key) { false }
it 'shows details' do
page.within('.breadcrumbs .breadcrumb-item-text') do
expect(page).to have_content(project.title)
end
expect(page).to have_field('project_clone', with: project.http_url_to_repo) unless user_has_ssh_key
end
end
context 'when project is public' do
context 'when project is empty' do
set(:project) { create(:project_empty_repo, :public) }
context 'when not signed in' do
before do
visit(project_path(project))
end
include_examples 'shows details of empty project'
end
context 'when signed in' do
before do
sign_in(user)
end
context 'when user does not have ssh keys' do
before do
visit(project_path(project))
end
include_examples 'shows details of empty project'
end
context 'when user has ssh keys' do
before do
create(:personal_key, user: user)
visit(project_path(project))
end
include_examples 'shows details of empty project' do
let(:user_has_ssh_key) { true }
end
end
end
end
context 'when project is not empty' do
set(:project) { create(:project, :public, :repository) }
before do
visit(project_path(project))
end
context 'when not signed in' do
before do
allow(Gitlab.config.gitlab).to receive(:host).and_return('www.example.com')
end
include_examples 'shows details of non empty project'
end
context 'when signed in' do
before do
sign_in(user)
end
context 'when user does not have ssh keys' do
before do
visit(project_path(project))
end
include_examples 'shows details of non empty project'
end
context 'when user has ssh keys' do
before do
create(:personal_key, user: user)
visit(project_path(project))
end
include_examples 'shows details of non empty project' do
let(:user_has_ssh_key) { true }
end
end
end
end
end
context 'when project is internal' do
set(:project) { create(:project, :internal, :repository) }
context 'when not signed in' do
before do
visit(project_path(project))
end
include_examples 'redirects to the sign in page'
end
context 'when signed in' do
before do
sign_in(user)
visit(project_path(project))
end
include_examples 'shows details of non empty project'
end
end
context 'when project is private' do
set(:project) { create(:project, :private) }
before do
visit(project_path(project))
end
include_examples 'redirects to the sign in page'
end
end
/* eslint-disable space-before-function-paren, arrow-body-style */
import '~/gl_field_errors';
import GlFieldErrors from '~/gl_field_errors';
((global) => {
describe('GL Style Field Errors', function() {
preloadFixtures('static/gl_field_errors.html.raw');
describe('GL Style Field Errors', function() {
beforeEach(function() {
loadFixtures('static/gl_field_errors.html.raw');
const $form = this.$form = $('form.gl-show-field-errors');
this.fieldErrors = new global.GlFieldErrors($form);
});
beforeEach(function() {
loadFixtures('static/gl_field_errors.html.raw');
const $form = this.$form = $('form.gl-show-field-errors');
this.fieldErrors = new GlFieldErrors($form);
});
it('should select the correct input elements', function() {
expect(this.$form).toBeDefined();
expect(this.$form.length).toBe(1);
expect(this.fieldErrors).toBeDefined();
const inputs = this.fieldErrors.state.inputs;
expect(inputs.length).toBe(4);
});
it('should select the correct input elements', function() {
expect(this.$form).toBeDefined();
expect(this.$form.length).toBe(1);
expect(this.fieldErrors).toBeDefined();
const inputs = this.fieldErrors.state.inputs;
expect(inputs.length).toBe(4);
});
it('should ignore elements with custom error handling', function() {
const customErrorFlag = 'gl-field-error-ignore';
const customErrorElem = $(`.${customErrorFlag}`);
it('should ignore elements with custom error handling', function() {
const customErrorFlag = 'gl-field-error-ignore';
const customErrorElem = $(`.${customErrorFlag}`);
expect(customErrorElem.length).toBe(1);
expect(customErrorElem.length).toBe(1);
const customErrors = this.fieldErrors.state.inputs.filter((input) => {
return input.inputElement.hasClass(customErrorFlag);
});
expect(customErrors.length).toBe(0);
const customErrors = this.fieldErrors.state.inputs.filter((input) => {
return input.inputElement.hasClass(customErrorFlag);
});
expect(customErrors.length).toBe(0);
});
it('should not show any errors before submit attempt', function() {
this.$form.find('.email').val('not-a-valid-email').keyup();
this.$form.find('.text-required').val('').keyup();
this.$form.find('.alphanumberic').val('?---*').keyup();
it('should not show any errors before submit attempt', function() {
this.$form.find('.email').val('not-a-valid-email').keyup();
this.$form.find('.text-required').val('').keyup();
this.$form.find('.alphanumberic').val('?---*').keyup();
const errorsShown = this.$form.find('.gl-field-error-outline');
expect(errorsShown.length).toBe(0);
});
const errorsShown = this.$form.find('.gl-field-error-outline');
expect(errorsShown.length).toBe(0);
});
it('should show errors when input valid is submitted', function() {
this.$form.find('.email').val('not-a-valid-email').keyup();
this.$form.find('.text-required').val('').keyup();
this.$form.find('.alphanumberic').val('?---*').keyup();
it('should show errors when input valid is submitted', function() {
this.$form.find('.email').val('not-a-valid-email').keyup();
this.$form.find('.text-required').val('').keyup();
this.$form.find('.alphanumberic').val('?---*').keyup();
this.$form.submit();
this.$form.submit();
const errorsShown = this.$form.find('.gl-field-error-outline');
expect(errorsShown.length).toBe(4);
});
const errorsShown = this.$form.find('.gl-field-error-outline');
expect(errorsShown.length).toBe(4);
});
it('should properly track validity state on input after invalid submission attempt', function() {
this.$form.submit();
const emailInputModel = this.fieldErrors.state.inputs[1];
const fieldState = emailInputModel.state;
const emailInputElement = emailInputModel.inputElement;
// No input
expect(emailInputElement).toHaveClass('gl-field-error-outline');
expect(fieldState.empty).toBe(true);
expect(fieldState.valid).toBe(false);
// Then invalid input
emailInputElement.val('not-a-valid-email').keyup();
expect(emailInputElement).toHaveClass('gl-field-error-outline');
expect(fieldState.empty).toBe(false);
expect(fieldState.valid).toBe(false);
// Then valid input
emailInputElement.val('email@gitlab.com').keyup();
expect(emailInputElement).not.toHaveClass('gl-field-error-outline');
expect(fieldState.empty).toBe(false);
expect(fieldState.valid).toBe(true);
// Then invalid input
emailInputElement.val('not-a-valid-email').keyup();
expect(emailInputElement).toHaveClass('gl-field-error-outline');
expect(fieldState.empty).toBe(false);
expect(fieldState.valid).toBe(false);
// Then empty input
emailInputElement.val('').keyup();
expect(emailInputElement).toHaveClass('gl-field-error-outline');
expect(fieldState.empty).toBe(true);
expect(fieldState.valid).toBe(false);
// Then valid input
emailInputElement.val('email@gitlab.com').keyup();
expect(emailInputElement).not.toHaveClass('gl-field-error-outline');
expect(fieldState.empty).toBe(false);
expect(fieldState.valid).toBe(true);
});
it('should properly track validity state on input after invalid submission attempt', function() {
this.$form.submit();
const emailInputModel = this.fieldErrors.state.inputs[1];
const fieldState = emailInputModel.state;
const emailInputElement = emailInputModel.inputElement;
// No input
expect(emailInputElement).toHaveClass('gl-field-error-outline');
expect(fieldState.empty).toBe(true);
expect(fieldState.valid).toBe(false);
// Then invalid input
emailInputElement.val('not-a-valid-email').keyup();
expect(emailInputElement).toHaveClass('gl-field-error-outline');
expect(fieldState.empty).toBe(false);
expect(fieldState.valid).toBe(false);
// Then valid input
emailInputElement.val('email@gitlab.com').keyup();
expect(emailInputElement).not.toHaveClass('gl-field-error-outline');
expect(fieldState.empty).toBe(false);
expect(fieldState.valid).toBe(true);
// Then invalid input
emailInputElement.val('not-a-valid-email').keyup();
expect(emailInputElement).toHaveClass('gl-field-error-outline');
expect(fieldState.empty).toBe(false);
expect(fieldState.valid).toBe(false);
// Then empty input
emailInputElement.val('').keyup();
expect(emailInputElement).toHaveClass('gl-field-error-outline');
expect(fieldState.empty).toBe(true);
expect(fieldState.valid).toBe(false);
// Then valid input
emailInputElement.val('email@gitlab.com').keyup();
expect(emailInputElement).not.toHaveClass('gl-field-error-outline');
expect(fieldState.empty).toBe(false);
expect(fieldState.valid).toBe(true);
});
it('should properly infer error messages', function() {
this.$form.submit();
const trackedInputs = this.fieldErrors.state.inputs;
const inputHasTitle = trackedInputs[1];
const hasTitleErrorElem = inputHasTitle.inputElement.siblings('.gl-field-error');
const inputNoTitle = trackedInputs[2];
const noTitleErrorElem = inputNoTitle.inputElement.siblings('.gl-field-error');
it('should properly infer error messages', function() {
this.$form.submit();
const trackedInputs = this.fieldErrors.state.inputs;
const inputHasTitle = trackedInputs[1];
const hasTitleErrorElem = inputHasTitle.inputElement.siblings('.gl-field-error');
const inputNoTitle = trackedInputs[2];
const noTitleErrorElem = inputNoTitle.inputElement.siblings('.gl-field-error');
expect(noTitleErrorElem.text()).toBe('This field is required.');
expect(hasTitleErrorElem.text()).toBe('Please provide a valid email address.');
});
expect(noTitleErrorElem.text()).toBe('This field is required.');
expect(hasTitleErrorElem.text()).toBe('Please provide a valid email address.');
});
})(window.gl || (window.gl = {}));
});
import autosize from 'vendor/autosize';
import '~/gl_form';
import GLForm from '~/gl_form';
import '~/lib/utils/text_utility';
import '~/lib/utils/common_utils';
window.autosize = autosize;
describe('GLForm', () => {
const global = window.gl || (window.gl = {});
const GLForm = global.GLForm;
it('should be defined in the global scope', () => {
expect(GLForm).toBeDefined();
});
describe('when instantiated', function () {
beforeEach((done) => {
this.form = $('<form class="gfm-form"><textarea class="js-gfm-input"></form>');
......
export default (time = 0) => new Promise((resolve) => {
setTimeout(resolve, time);
});
......@@ -431,19 +431,17 @@ import '~/notes';
});
describe('putEditFormInPlace', () => {
it('should call gl.GLForm with GFM parameter passed through', () => {
spyOn(gl, 'GLForm');
it('should call GLForm with GFM parameter passed through', () => {
const notes = new Notes('', []);
const $el = $(`
<div>
<form></form>
</div>
`);
const $el = jasmine.createSpyObj('$form', ['find', 'closest']);
$el.find.and.returnValue($('<div>'));
$el.closest.and.returnValue($('<div>'));
notes.putEditFormInPlace($el);
Notes.prototype.putEditFormInPlace.call({
getEditFormSelector: () => '',
enableGFM: true
}, $el);
expect(gl.GLForm).toHaveBeenCalledWith(jasmine.any(Object), true);
expect(notes.glForm.enableGFM).toBeTruthy();
});
});
......
......@@ -109,12 +109,16 @@ describe('PrometheusMetrics', () => {
it('should show loader animation while response is being loaded and hide it when request is complete', (done) => {
const deferred = $.Deferred();
spyOn($, 'getJSON').and.returnValue(deferred.promise());
spyOn($, 'ajax').and.returnValue(deferred.promise());
prometheusMetrics.loadActiveMetrics();
expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeFalsy();
expect($.getJSON).toHaveBeenCalledWith(prometheusMetrics.activeMetricsEndpoint);
expect($.ajax).toHaveBeenCalledWith({
url: prometheusMetrics.activeMetricsEndpoint,
dataType: 'json',
global: false,
});
deferred.resolve({ data: metrics, success: true });
......@@ -126,7 +130,7 @@ describe('PrometheusMetrics', () => {
it('should show empty state if response failed to load', (done) => {
const deferred = $.Deferred();
spyOn($, 'getJSON').and.returnValue(deferred.promise());
spyOn($, 'ajax').and.returnValue(deferred.promise());
spyOn(prometheusMetrics, 'populateActiveMetrics');
prometheusMetrics.loadActiveMetrics();
......@@ -142,7 +146,7 @@ describe('PrometheusMetrics', () => {
it('should populate metrics list once response is loaded', (done) => {
const deferred = $.Deferred();
spyOn($, 'getJSON').and.returnValue(deferred.promise());
spyOn($, 'ajax').and.returnValue(deferred.promise());
spyOn(prometheusMetrics, 'populateActiveMetrics');
prometheusMetrics.loadActiveMetrics();
......
......@@ -2,29 +2,13 @@ import Vue from 'vue';
import repoCommitSection from '~/repo/components/repo_commit_section.vue';
import RepoStore from '~/repo/stores/repo_store';
import RepoService from '~/repo/services/repo_service';
import getSetTimeoutPromise from '../../helpers/set_timeout_promise_helper';
describe('RepoCommitSection', () => {
const branch = 'master';
const projectUrl = 'projectUrl';
const changedFiles = [{
id: 0,
changed: true,
url: `/namespace/${projectUrl}/blob/${branch}/dir/file0.ext`,
path: 'dir/file0.ext',
newContent: 'a',
}, {
id: 1,
changed: true,
url: `/namespace/${projectUrl}/blob/${branch}/dir/file1.ext`,
path: 'dir/file1.ext',
newContent: 'b',
}];
const openedFiles = changedFiles.concat([{
id: 2,
url: `/namespace/${projectUrl}/blob/${branch}/dir/file2.ext`,
path: 'dir/file2.ext',
changed: false,
}]);
let changedFiles;
let openedFiles;
RepoStore.projectUrl = projectUrl;
......@@ -34,6 +18,29 @@ describe('RepoCommitSection', () => {
return new RepoCommitSection().$mount(el);
}
beforeEach(() => {
// Create a copy for each test because these can get modified directly
changedFiles = [{
id: 0,
changed: true,
url: `/namespace/${projectUrl}/blob/${branch}/dir/file0.ext`,
path: 'dir/file0.ext',
newContent: 'a',
}, {
id: 1,
changed: true,
url: `/namespace/${projectUrl}/blob/${branch}/dir/file1.ext`,
path: 'dir/file1.ext',
newContent: 'b',
}];
openedFiles = changedFiles.concat([{
id: 2,
url: `/namespace/${projectUrl}/blob/${branch}/dir/file2.ext`,
path: 'dir/file2.ext',
changed: false,
}]);
});
it('renders a commit section', () => {
RepoStore.isCommitable = true;
RepoStore.currentBranch = branch;
......@@ -85,55 +92,104 @@ describe('RepoCommitSection', () => {
expect(vm.$el.innerHTML).toBeFalsy();
});
it('shows commit submit and summary if commitMessage and spinner if submitCommitsLoading', (done) => {
describe('when submitting', () => {
let el;
let vm;
const projectId = 'projectId';
const commitMessage = 'commitMessage';
RepoStore.isCommitable = true;
RepoStore.currentBranch = branch;
RepoStore.targetBranch = branch;
RepoStore.openedFiles = openedFiles;
RepoStore.projectId = projectId;
// We need to append to body to get form `submit` events working
// Otherwise we run into, "Form submission canceled because the form is not connected"
// See https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#form-submission-algorithm
const el = document.createElement('div');
document.body.appendChild(el);
const vm = createComponent(el);
const commitMessageEl = vm.$el.querySelector('#commit-message');
const submitCommit = vm.$refs.submitCommit;
beforeEach((done) => {
RepoStore.isCommitable = true;
RepoStore.currentBranch = branch;
RepoStore.targetBranch = branch;
RepoStore.openedFiles = openedFiles;
RepoStore.projectId = projectId;
// We need to append to body to get form `submit` events working
// Otherwise we run into, "Form submission canceled because the form is not connected"
// See https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#form-submission-algorithm
el = document.createElement('div');
document.body.appendChild(el);
vm = createComponent(el);
vm.commitMessage = commitMessage;
spyOn(vm, 'tryCommit').and.callThrough();
spyOn(vm, 'redirectToNewMr').and.stub();
spyOn(vm, 'redirectToBranch').and.stub();
spyOn(RepoService, 'commitFiles').and.returnValue(Promise.resolve());
spyOn(RepoService, 'getBranch').and.returnValue(Promise.resolve({
commit: {
id: 1,
short_id: 1,
},
}));
// Wait for the vm data to be in place
Vue.nextTick(() => {
done();
});
});
vm.commitMessage = commitMessage;
afterEach(() => {
vm.$destroy();
el.remove();
});
Vue.nextTick(() => {
it('shows commit message', () => {
const commitMessageEl = vm.$el.querySelector('#commit-message');
expect(commitMessageEl.value).toBe(commitMessage);
expect(submitCommit.disabled).toBeFalsy();
});
spyOn(vm, 'makeCommit').and.callThrough();
spyOn(RepoService, 'commitFiles').and.callFake(() => Promise.resolve());
it('allows you to submit', () => {
const submitCommit = vm.$refs.submitCommit;
expect(submitCommit.disabled).toBeFalsy();
});
it('shows commit submit and summary if commitMessage and spinner if submitCommitsLoading', (done) => {
const submitCommit = vm.$refs.submitCommit;
submitCommit.click();
Vue.nextTick(() => {
expect(vm.makeCommit).toHaveBeenCalled();
expect(submitCommit.querySelector('.fa-spinner.fa-spin')).toBeTruthy();
const args = RepoService.commitFiles.calls.allArgs()[0];
const { commit_message, actions, branch: payloadBranch } = args[0];
expect(commit_message).toBe(commitMessage);
expect(actions.length).toEqual(2);
expect(payloadBranch).toEqual(branch);
expect(actions[0].action).toEqual('update');
expect(actions[1].action).toEqual('update');
expect(actions[0].content).toEqual(openedFiles[0].newContent);
expect(actions[1].content).toEqual(openedFiles[1].newContent);
expect(actions[0].file_path).toEqual(openedFiles[0].path);
expect(actions[1].file_path).toEqual(openedFiles[1].path);
// Wait for the branch check to finish
getSetTimeoutPromise()
.then(() => Vue.nextTick())
.then(() => {
expect(vm.tryCommit).toHaveBeenCalled();
expect(submitCommit.querySelector('.js-commit-loading-icon')).toBeTruthy();
expect(vm.redirectToBranch).toHaveBeenCalled();
const args = RepoService.commitFiles.calls.allArgs()[0];
const { commit_message, actions, branch: payloadBranch } = args[0];
expect(commit_message).toBe(commitMessage);
expect(actions.length).toEqual(2);
expect(payloadBranch).toEqual(branch);
expect(actions[0].action).toEqual('update');
expect(actions[1].action).toEqual('update');
expect(actions[0].content).toEqual(openedFiles[0].newContent);
expect(actions[1].content).toEqual(openedFiles[1].newContent);
expect(actions[0].file_path).toEqual(openedFiles[0].path);
expect(actions[1].file_path).toEqual(openedFiles[1].path);
})
.then(done)
.catch(done.fail);
});
done();
});
it('redirects to MR creation page if start new MR checkbox checked', (done) => {
vm.startNewMR = true;
Vue.nextTick()
.then(() => {
const submitCommit = vm.$refs.submitCommit;
submitCommit.click();
})
// Wait for the branch check to finish
.then(() => getSetTimeoutPromise())
.then(() => {
expect(vm.redirectToNewMr).toHaveBeenCalled();
})
.then(done)
.catch(done.fail);
});
});
......@@ -143,6 +199,7 @@ describe('RepoCommitSection', () => {
const vm = {
submitCommitsLoading: true,
changedFiles: new Array(10),
openedFiles: new Array(3),
commitMessage: 'commitMessage',
editMode: true,
};
......
......@@ -18,6 +18,10 @@ describe Gitlab::FileDetector do
expect(described_class.type_of('README.md')).to eq(:readme)
end
it 'returns nil for a README file in a directory' do
expect(described_class.type_of('foo/README.md')).to be_nil
end
it 'returns the type of a changelog file' do
%w(CHANGELOG HISTORY CHANGES NEWS).each do |file|
expect(described_class.type_of(file)).to eq(:changelog)
......@@ -52,6 +56,14 @@ describe Gitlab::FileDetector do
end
end
it 'returns the type of an issue template' do
expect(described_class.type_of('.gitlab/issue_templates/foo.md')).to eq(:issue_template)
end
it 'returns the type of a merge request template' do
expect(described_class.type_of('.gitlab/merge_request_templates/foo.md')).to eq(:merge_request_template)
end
it 'returns nil for an unknown file' do
expect(described_class.type_of('foo.txt')).to be_nil
end
......
......@@ -1510,6 +1510,21 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
end
describe '#fetch' do
let(:git_path) { Gitlab.config.git.bin_path }
let(:remote_name) { 'my_remote' }
subject { repository.fetch(remote_name) }
it 'fetches the remote and returns true if the command was successful' do
expect(repository).to receive(:popen)
.with(%W(#{git_path} fetch #{remote_name}), repository.path)
.and_return(['', 0])
expect(subject).to be(true)
end
end
def create_remote_branch(repository, remote_name, branch_name, source_branch_name)
source_branch = repository.branches.find { |branch| branch.name == source_branch_name }
rugged = repository.rugged
......
......@@ -1509,7 +1509,9 @@ describe Repository do
:gitignore,
:koding,
:gitlab_ci,
:avatar
:avatar,
:issue_template,
:merge_request_template
])
repository.after_change_head
......
......@@ -227,13 +227,6 @@ describe API::Helpers do
expect { current_user }.to raise_error /401/
end
it "returns a 401 response for a token without the appropriate scope" do
personal_access_token = create(:personal_access_token, user: user, scopes: ['read_user'])
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
expect { current_user }.to raise_error /401/
end
it "leaves user as is when sudo not specified" do
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
expect(current_user).to eq(user)
......@@ -243,18 +236,25 @@ describe API::Helpers do
expect(current_user).to eq(user)
end
it "does not allow tokens without the appropriate scope" do
personal_access_token = create(:personal_access_token, user: user, scopes: ['read_user'])
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
expect { current_user }.to raise_error API::APIGuard::InsufficientScopeError
end
it 'does not allow revoked tokens' do
personal_access_token.revoke!
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
expect { current_user }.to raise_error /401/
expect { current_user }.to raise_error API::APIGuard::RevokedError
end
it 'does not allow expired tokens' do
personal_access_token.update_attributes!(expires_at: 1.day.ago)
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
expect { current_user }.to raise_error /401/
expect { current_user }.to raise_error API::APIGuard::ExpiredError
end
end
......
......@@ -48,6 +48,21 @@ describe Auth::ContainerRegistryAuthenticationService do
end
end
shared_examples 'a browsable' do
let(:access) do
[{ 'type' => 'registry',
'name' => 'catalog',
'actions' => ['*'] }]
end
it_behaves_like 'a valid token'
it_behaves_like 'not a container repository factory'
it 'has the correct scope' do
expect(payload).to include('access' => access)
end
end
shared_examples 'an accessible' do
let(:access) do
[{ 'type' => 'repository',
......@@ -56,7 +71,10 @@ describe Auth::ContainerRegistryAuthenticationService do
end
it_behaves_like 'a valid token'
it { expect(payload).to include('access' => access) }
it 'has the correct scope' do
expect(payload).to include('access' => access)
end
end
shared_examples 'an inaccessible' do
......@@ -122,6 +140,17 @@ describe Auth::ContainerRegistryAuthenticationService do
context 'user authorization' do
let(:current_user) { create(:user) }
context 'for registry catalog' do
let(:current_params) do
{ scope: "registry:catalog:*" }
end
context 'disallow browsing for users without Gitlab admin rights' do
it_behaves_like 'an inaccessible'
it_behaves_like 'not a container repository factory'
end
end
context 'for private project' do
let(:project) { create(:project) }
......@@ -503,6 +532,16 @@ describe Auth::ContainerRegistryAuthenticationService do
end
end
context 'registry catalog browsing authorized as admin' do
let(:current_user) { create(:user, :admin) }
let(:current_params) do
{ scope: "registry:catalog:*" }
end
it_behaves_like 'a browsable'
end
context 'unauthorized' do
context 'disallow to use scope-less authentication' do
it_behaves_like 'a forbidden'
......@@ -549,5 +588,14 @@ describe Auth::ContainerRegistryAuthenticationService do
it_behaves_like 'not a container repository factory'
end
end
context 'for registry catalog' do
let(:current_params) do
{ scope: "registry:catalog:*" }
end
it_behaves_like 'a forbidden'
it_behaves_like 'not a container repository factory'
end
end
end
......@@ -201,7 +201,7 @@ describe MergeRequests::MergeService do
context 'source branch removal' do
context 'when the source branch is protected' do
let(:service) do
described_class.new(project, user, should_remove_source_branch: '1')
described_class.new(project, user, 'should_remove_source_branch' => true)
end
before do
......@@ -216,7 +216,7 @@ describe MergeRequests::MergeService do
context 'when the source branch is the default branch' do
let(:service) do
described_class.new(project, user, should_remove_source_branch: '1')
described_class.new(project, user, 'should_remove_source_branch' => true)
end
before do
......@@ -231,10 +231,10 @@ describe MergeRequests::MergeService do
context 'when the source branch can be removed' do
context 'when MR author set the source branch to be removed' do
let(:service) do
merge_request.merge_params['force_remove_source_branch'] = '1'
merge_request.save!
described_class.new(project, user, commit_message: 'Awesome message')
let(:service) { described_class.new(project, user, commit_message: 'Awesome message') }
before do
merge_request.update_attribute(:merge_params, { 'force_remove_source_branch' => '1' })
end
it 'removes the source branch using the author user' do
......@@ -243,11 +243,20 @@ describe MergeRequests::MergeService do
.and_call_original
service.execute(merge_request)
end
context 'when the merger set the source branch not to be removed' do
let(:service) { described_class.new(project, user, commit_message: 'Awesome message', 'should_remove_source_branch' => false) }
it 'does not delete the source branch' do
expect(DeleteBranchService).not_to receive(:new)
service.execute(merge_request)
end
end
end
context 'when MR merger set the source branch to be removed' do
let(:service) do
described_class.new(project, user, commit_message: 'Awesome message', should_remove_source_branch: '1')
described_class.new(project, user, commit_message: 'Awesome message', 'should_remove_source_branch' => true)
end
it 'removes the source branch using the current user' do
......
......@@ -731,6 +731,18 @@ describe NotificationService, :mailer do
should_not_email(@u_participating)
end
it "doesn't send multiple email when a user is subscribed to multiple given labels" do
subscriber_to_both = create(:user) do |user|
[label_1, label_2].each { |label| label.toggle_subscription(user, project) }
end
notification.relabeled_issue(issue, [label_1, label_2], @u_disabled)
should_email(subscriber_to_label_1)
should_email(subscriber_to_label_2)
should_email(subscriber_to_both)
end
context 'confidential issues' do
let(:author) { create(:user) }
let(:assignee) { create(:user) }
......
......@@ -27,10 +27,10 @@ shared_examples_for 'allows the "read_user" scope' do
stub_container_registry_config(enabled: true)
end
it 'returns a "401" response' do
it 'returns a "403" response' do
get api_call.call(path, user, personal_access_token: token)
expect(response).to have_http_status(401)
expect(response).to have_http_status(403)
end
end
end
......@@ -74,10 +74,10 @@ shared_examples_for 'does not allow the "read_user" scope' do
context 'when the requesting token has the "read_user" scope' do
let(:token) { create(:personal_access_token, scopes: ['read_user'], user: user) }
it 'returns a "401" response' do
it 'returns a "403" response' do
post api_call.call(path, user, personal_access_token: token), attributes_for(:user, projects_limit: 3)
expect(response).to have_http_status(401)
expect(response).to have_http_status(403)
end
end
end
module EmailHelpers
def sent_to_user?(user, recipients = email_recipients)
recipients.include?(user.notification_email)
def sent_to_user(user, recipients: email_recipients)
recipients.count { |to| to == user.notification_email }
end
def reset_delivered_emails!
......@@ -10,17 +10,17 @@ module EmailHelpers
def should_only_email(*users, kind: :to)
recipients = email_recipients(kind: kind)
users.each { |user| should_email(user, recipients) }
users.each { |user| should_email(user, recipients: recipients) }
expect(recipients.count).to eq(users.count)
end
def should_email(user, recipients = email_recipients)
expect(sent_to_user?(user, recipients)).to be_truthy
def should_email(user, times: 1, recipients: email_recipients)
expect(sent_to_user(user, recipients: recipients)).to eq(times)
end
def should_not_email(user, recipients = email_recipients)
expect(sent_to_user?(user, recipients)).to be_falsey
def should_not_email(user, recipients: email_recipients)
should_email(user, times: 0, recipients: recipients)
end
def should_not_email_anyone
......
......@@ -41,7 +41,8 @@ captures/
.idea/libraries
# Keystore files
*.jks
# Uncomment the following line if you do not want to check your keystore files in.
#*.jks
# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
......
......@@ -31,3 +31,12 @@ Makefile.in
# http://www.gnu.org/software/texinfo
/texinfo.tex
# http://www.gnu.org/software/m4/
m4/libtool.m4
m4/ltoptions.m4
m4/ltsugar.m4
m4/ltversion.m4
m4/lt~obsolete.m4
autom4te.cache
/_build
/cover
/deps
/doc
/.fetch
erl_crash.dump
*.ez
*.beam
......@@ -10,3 +10,5 @@ ext/
modern.json
modern.jsonp
resources/sass/.sass-cache/
resources/.arch-internal-preview.css
.arch-internal-preview.css
......@@ -19,4 +19,4 @@ slprj/
octave-workspace
# Simulink autosave extension
.autosave
*.autosave
......@@ -2,11 +2,17 @@
#
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
## Build generated
## User settings
xcuserdata/
## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
*.xcscmblueprint
*.xccheckout
## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
build/
DerivedData/
## Various settings
*.moved-aside
*.pbxuser
!default.pbxuser
*.mode1v3
......@@ -15,9 +21,3 @@ DerivedData/
!default.mode2v3
*.perspectivev3
!default.perspectivev3
xcuserdata/
## Other
*.moved-aside
*.xccheckout
*.xcscmblueprint
# General
*.DS_Store
.DS_Store
.AppleDouble
.LSOverride
......
......@@ -251,7 +251,7 @@
/administrator/language/en-GB/en-GB.tpl_hathor.sys.ini
/administrator/language/en-GB/en-GB.xml
/administrator/language/overrides/*
/administrator/logs/index.html
/administrator/logs/*
/administrator/manifests/*
/administrator/modules/mod_custom/*
/administrator/modules/mod_feed/*
......
......@@ -18,3 +18,6 @@ _build/
# oasis generated files
setup.data
setup.log
# Merlin configuring file for Vim and Emacs
.merlin
......@@ -23,6 +23,7 @@ wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
......@@ -51,6 +52,8 @@ coverage.xml
# Django stuff:
*.log
.static_storage/
.media/
local_settings.py
# Flask stuff:
......@@ -84,6 +87,8 @@ celerybeat-schedule
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
......
......@@ -31,11 +31,9 @@ ui_*.h
Makefile*
*build-*
# Qt unit tests
target_wrapper.*
# QtCreator
*.autosave
......
......@@ -13,6 +13,7 @@
## Intermediate documents:
*.dvi
*.xdv
*-converted-to.*
# these rules might exclude image files for figures etc.
# *.ps
......
......@@ -5,3 +5,6 @@
# Module directory
.terraform/
# Variable values for development
terraform.tfvars
## Ignore Umbraco files/folders generated for each instance
##
## Get latest from https://github.com/github/gitignore/blob/master/Umbraco.gitignore
# Note: VisualStudio gitignore rules may also be relevant
# Umbraco
......
......@@ -96,6 +96,9 @@ ipch/
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
......@@ -297,3 +300,6 @@ __pycache__/
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
......@@ -19,7 +19,6 @@ temp/
data/DoctrineORMModule/Proxy/
data/DoctrineORMModule/cache/
# Legacy ZF1
demos/
extras/documentation
......@@ -29,7 +29,7 @@ format:
compile:
stage: build
script:
- go build -race -ldflags "-extldflags '-static'" -o mybinary
- go build -race -ldflags "-extldflags '-static'" -o $CI_PROJECT_DIR/mybinary
artifacts:
paths:
- mybinary
......@@ -7,8 +7,8 @@
# This template will build and test your projects as well as create the documentation.
#
# * Caches downloaded dependencies and plugins between invocation.
# * Does only verify merge requests but deploy built artifacts of the
# master branch.
# * Verify but don't deploy merge requests.
# * Deploy built artifacts from master branch only.
# * Shows how to use multiple jobs in test stage for verifying functionality
# with multiple JDKs.
# * Uses site:stage to collect the documentation for multi-module projects.
......@@ -20,7 +20,7 @@ variables:
MAVEN_OPTS: "-Dmaven.repo.local=.m2/repository -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=WARN -Dorg.slf4j.simpleLogger.showDateTime=true -Djava.awt.headless=true"
# As of Maven 3.3.0 instead of this you may define these options in `.mvn/maven.config` so the same config is used
# when running from the command line.
# `installAtEnd` and `deployAtEnd`are only effective with recent version of the corresponding plugins.
# `installAtEnd` and `deployAtEnd` are only effective with recent version of the corresponding plugins.
MAVEN_CLI_OPTS: "--batch-mode --errors --fail-at-end --show-version -DinstallAtEnd=true -DdeployAtEnd=true"
# Cache downloaded dependencies and plugins between builds.
......@@ -100,4 +100,3 @@ pages:
- public
only:
- master
# This file is a template, and might need editing before it works on your project.
image: python:latest
before_script:
- python -V # Print out python version for debugging
test:
script:
- python setup.py test
- pip install tox flake8 # you can also use tox
- tox -e py36,flake8
run:
script:
- python setup.py bdist_wheel
# an alternative approach is to install and run:
- pip install dist/*
# run the command here
artifacts:
paths:
- dist/*.whl
pages:
script:
- pip install sphinx sphinx-rtd-theme
- cd doc ; make html
- mv build/html/ ../public/
artifacts:
paths:
- public
only:
- master
This diff is collapsed.
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