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 = { ...@@ -16,6 +16,7 @@ const Api = {
issuableTemplatePath: '/:namespace_path/:project_path/templates/:type/:key', issuableTemplatePath: '/:namespace_path/:project_path/templates/:type/:key',
usersPath: '/api/:version/users.json', usersPath: '/api/:version/users.json',
commitPath: '/api/:version/projects/:id/repository/commits', commitPath: '/api/:version/projects/:id/repository/commits',
branchSinglePath: '/api/:version/projects/:id/repository/branches/:branch',
group(groupId, callback) { group(groupId, callback) {
const url = Api.buildUrl(Api.groupPath) const url = Api.buildUrl(Api.groupPath)
...@@ -124,6 +125,19 @@ const Api = { ...@@ -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 // Return text for a specific license
licenseText(key, data, callback) { licenseText(key, data, callback) {
const url = Api.buildUrl(Api.licensePath) 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 */ export default function initBroadcastMessagesForm() {
$('input#broadcast_message_color').on('input', function onMessageColorInput() {
$(function() { const previewColor = $(this).val();
var previewPath; $('div.broadcast-message-preview').css('background-color', previewColor);
$('input#broadcast_message_color').on('input', function() {
var previewColor;
previewColor = $(this).val();
return $('div.broadcast-message-preview').css('background-color', previewColor);
}); });
$('input#broadcast_message_font').on('input', function() {
var previewColor; $('input#broadcast_message_font').on('input', function onMessageFontInput() {
previewColor = $(this).val(); const previewColor = $(this).val();
return $('div.broadcast-message-preview').css('color', previewColor); $('div.broadcast-message-preview').css('color', previewColor);
}); });
previewPath = $('textarea#broadcast_message_message').data('preview-path');
return $('textarea#broadcast_message_message').on('input', function() { const previewPath = $('textarea#broadcast_message_message').data('preview-path');
var message;
message = $(this).val(); $('textarea#broadcast_message_message').on('input', _.debounce(function onMessageInput() {
const message = $(this).val();
if (message === '') { if (message === '') {
return $('.js-broadcast-message-preview').text("Your message here"); $('.js-broadcast-message-preview').text('Your message here');
} else { } else {
return $.ajax({ $.ajax({
url: previewPath, url: previewPath,
type: "POST", type: 'POST',
data: { data: {
broadcast_message: { broadcast_message: { message },
message: message },
}
}
}); });
} }
}); }, 250));
}); }
...@@ -68,6 +68,7 @@ import initSettingsPanels from './settings_panels'; ...@@ -68,6 +68,7 @@ import initSettingsPanels from './settings_panels';
import initExperimentalFlags from './experimental_flags'; import initExperimentalFlags from './experimental_flags';
import OAuthRememberMe from './oauth_remember_me'; import OAuthRememberMe from './oauth_remember_me';
import PerformanceBar from './performance_bar'; import PerformanceBar from './performance_bar';
import initBroadcastMessagesForm from './broadcast_message';
import initNotes from './init_notes'; import initNotes from './init_notes';
import initLegacyFilters from './init_legacy_filters'; import initLegacyFilters from './init_legacy_filters';
import initIssuableSidebar from './init_issuable_sidebar'; import initIssuableSidebar from './init_issuable_sidebar';
...@@ -78,11 +79,15 @@ import initChangesDropdown from './init_changes_dropdown'; ...@@ -78,11 +79,15 @@ import initChangesDropdown from './init_changes_dropdown';
import AbuseReports from './abuse_reports'; import AbuseReports from './abuse_reports';
import { ajaxGet, convertPermissionToBoolean } from './lib/utils/common_utils'; import { ajaxGet, convertPermissionToBoolean } from './lib/utils/common_utils';
import AjaxLoadingSpinner from './ajax_loading_spinner'; import AjaxLoadingSpinner from './ajax_loading_spinner';
import GlFieldErrors from './gl_field_errors';
import GLForm from './gl_form';
import Shortcuts from './shortcuts'; import Shortcuts from './shortcuts';
import ShortcutsNavigation from './shortcuts_navigation'; import ShortcutsNavigation from './shortcuts_navigation';
import ShortcutsFindFile from './shortcuts_find_file'; import ShortcutsFindFile from './shortcuts_find_file';
import ShortcutsIssuable from './shortcuts_issuable'; import ShortcutsIssuable from './shortcuts_issuable';
import U2FAuthenticate from './u2f/authenticate'; import U2FAuthenticate from './u2f/authenticate';
import Members from './members';
import memberExpirationDate from './member_expiration_date';
// EE-only // EE-only
import ApproversSelect from './approvers_select'; import ApproversSelect from './approvers_select';
...@@ -252,7 +257,7 @@ import initGroupAnalytics from './init_group_analytics'; ...@@ -252,7 +257,7 @@ import initGroupAnalytics from './init_group_analytics';
case 'groups:milestones:update': case 'groups:milestones:update':
new ZenMode(); new ZenMode();
new gl.DueDateSelectors(); new gl.DueDateSelectors();
new gl.GLForm($('.milestone-form'), true); new GLForm($('.milestone-form'), true);
break; break;
case 'projects:compare:show': case 'projects:compare:show':
new gl.Diff(); new gl.Diff();
...@@ -269,7 +274,7 @@ import initGroupAnalytics from './init_group_analytics'; ...@@ -269,7 +274,7 @@ import initGroupAnalytics from './init_group_analytics';
case 'projects:issues:new': case 'projects:issues:new':
case 'projects:issues:edit': case 'projects:issues:edit':
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
new gl.GLForm($('.issue-form'), true); new GLForm($('.issue-form'), true);
new IssuableForm($('.issue-form')); new IssuableForm($('.issue-form'));
new LabelsSelect(); new LabelsSelect();
new MilestoneSelect(); new MilestoneSelect();
...@@ -295,7 +300,7 @@ import initGroupAnalytics from './init_group_analytics'; ...@@ -295,7 +300,7 @@ import initGroupAnalytics from './init_group_analytics';
case 'projects:merge_requests:edit': case 'projects:merge_requests:edit':
new gl.Diff(); new gl.Diff();
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
new gl.GLForm($('.merge-request-form'), true); new GLForm($('.merge-request-form'), true);
new IssuableForm($('.merge-request-form')); new IssuableForm($('.merge-request-form'));
new LabelsSelect(); new LabelsSelect();
new MilestoneSelect(); new MilestoneSelect();
...@@ -304,7 +309,7 @@ import initGroupAnalytics from './init_group_analytics'; ...@@ -304,7 +309,7 @@ import initGroupAnalytics from './init_group_analytics';
break; break;
case 'projects:tags:new': case 'projects:tags:new':
new ZenMode(); new ZenMode();
new gl.GLForm($('.tag-form'), true); new GLForm($('.tag-form'), true);
new RefSelectDropdown($('.js-branch-select')); new RefSelectDropdown($('.js-branch-select'));
break; break;
case 'projects:snippets:show': case 'projects:snippets:show':
...@@ -314,17 +319,17 @@ import initGroupAnalytics from './init_group_analytics'; ...@@ -314,17 +319,17 @@ import initGroupAnalytics from './init_group_analytics';
case 'projects:snippets:edit': case 'projects:snippets:edit':
case 'projects:snippets:create': case 'projects:snippets:create':
case 'projects:snippets:update': case 'projects:snippets:update':
new gl.GLForm($('.snippet-form'), true); new GLForm($('.snippet-form'), true);
break; break;
case 'snippets:new': case 'snippets:new':
case 'snippets:edit': case 'snippets:edit':
case 'snippets:create': case 'snippets:create':
case 'snippets:update': case 'snippets:update':
new gl.GLForm($('.snippet-form'), false); new GLForm($('.snippet-form'), false);
break; break;
case 'projects:releases:edit': case 'projects:releases:edit':
new ZenMode(); new ZenMode();
new gl.GLForm($('.release-form'), true); new GLForm($('.release-form'), true);
break; break;
case 'projects:merge_requests:show': case 'projects:merge_requests:show':
new gl.Diff(); new gl.Diff();
...@@ -430,15 +435,15 @@ import initGroupAnalytics from './init_group_analytics'; ...@@ -430,15 +435,15 @@ import initGroupAnalytics from './init_group_analytics';
new ProjectsList(); new ProjectsList();
break; break;
case 'groups:group_members:index': case 'groups:group_members:index':
new gl.MemberExpirationDate(); memberExpirationDate();
new gl.Members(); new Members();
new UsersSelect(); new UsersSelect();
break; break;
case 'projects:project_members:index': case 'projects:project_members:index':
new gl.MemberExpirationDate('.js-access-expiration-date-groups'); memberExpirationDate('.js-access-expiration-date-groups');
new GroupsSelect(); new GroupsSelect();
new gl.MemberExpirationDate(); memberExpirationDate();
new gl.Members(); new Members();
new UsersSelect(); new UsersSelect();
break; break;
case 'groups:new': case 'groups:new':
...@@ -621,6 +626,9 @@ import initGroupAnalytics from './init_group_analytics'; ...@@ -621,6 +626,9 @@ import initGroupAnalytics from './init_group_analytics';
case 'admin': case 'admin':
new Admin(); new Admin();
switch (path[1]) { switch (path[1]) {
case 'broadcast_messages':
initBroadcastMessagesForm();
break;
case 'cohorts': case 'cohorts':
new UsagePing(); new UsagePing();
break; break;
...@@ -684,7 +692,7 @@ import initGroupAnalytics from './init_group_analytics'; ...@@ -684,7 +692,7 @@ import initGroupAnalytics from './init_group_analytics';
new Wikis(); new Wikis();
shortcut_handler = new ShortcutsWiki(); shortcut_handler = new ShortcutsWiki();
new ZenMode(); new ZenMode();
new gl.GLForm($('.wiki-form'), true); new GLForm($('.wiki-form'), true);
break; break;
case 'snippets': case 'snippets':
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
...@@ -709,12 +717,6 @@ import initGroupAnalytics from './init_group_analytics'; ...@@ -709,12 +717,6 @@ import initGroupAnalytics from './init_group_analytics';
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
} }
break; 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 we haven't installed a custom shortcut handler, install the default one
if (!shortcut_handler) { if (!shortcut_handler) {
...@@ -735,7 +737,7 @@ import initGroupAnalytics from './init_group_analytics'; ...@@ -735,7 +737,7 @@ import initGroupAnalytics from './init_group_analytics';
Dispatcher.prototype.initFieldErrors = function() { Dispatcher.prototype.initFieldErrors = function() {
$('.gl-show-field-errors').each((i, form) => { $('.gl-show-field-errors').each((i, form) => {
new gl.GlFieldErrors(form); new GlFieldErrors(form);
}); });
}; };
......
...@@ -54,7 +54,7 @@ const inputErrorClass = 'gl-field-error-outline'; ...@@ -54,7 +54,7 @@ const inputErrorClass = 'gl-field-error-outline';
const errorAnchorSelector = '.gl-field-error-anchor'; const errorAnchorSelector = '.gl-field-error-anchor';
const ignoreInputSelector = '.gl-field-error-ignore'; const ignoreInputSelector = '.gl-field-error-ignore';
class GlFieldError { export default class GlFieldError {
constructor({ input, formErrors }) { constructor({ input, formErrors }) {
this.inputElement = $(input); this.inputElement = $(input);
this.inputDomElement = this.inputElement.get(0); this.inputDomElement = this.inputElement.get(0);
...@@ -159,6 +159,3 @@ class GlFieldError { ...@@ -159,6 +159,3 @@ class GlFieldError {
this.fieldErrorElement.hide(); 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 GlFieldError from './gl_field_error';
import './gl_field_error';
const customValidationFlag = 'gl-field-error-ignore'; const customValidationFlag = 'gl-field-error-ignore';
class GlFieldErrors { export default class GlFieldErrors {
constructor(form) { constructor(form) {
this.form = $(form); this.form = $(form);
this.state = { this.state = {
inputs: [], inputs: [],
valid: false valid: false,
}; };
this.initValidators(); this.initValidators();
} }
initValidators () { initValidators() {
// register selectors here as needed // register selectors here as needed
const validateSelectors = [':text', ':password', '[type=email]'] const validateSelectors = [':text', ':password', '[type=email]']
.map((selector) => `input${selector}`).join(','); .map(selector => `input${selector}`).join(',');
this.state.inputs = this.form.find(validateSelectors).toArray() this.state.inputs = this.form.find(validateSelectors).toArray()
.filter((input) => !input.classList.contains(customValidationFlag)) .filter(input => !input.classList.contains(customValidationFlag))
.map((input) => new window.gl.GlFieldError({ input, formErrors: this })); .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 /* Neccessary to prevent intercept and override invalid form submit
* because Safari & iOS quietly allow form submission when form is invalid * because Safari & iOS quietly allow form submission when form is invalid
* and prevents disabling of invalid submit button by application.js */ * and prevents disabling of invalid submit button by application.js */
catchInvalidFormSubmit (event) { static catchInvalidFormSubmit(e) {
const $form = $(event.currentTarget); const $form = $(e.currentTarget);
if (!$form.attr('novalidate')) { if (!$form.attr('novalidate')) {
if (!event.currentTarget.checkValidity()) { if (!e.currentTarget.checkValidity()) {
event.preventDefault(); e.preventDefault();
event.stopPropagation(); e.stopPropagation();
} }
} }
} }
...@@ -50,11 +48,9 @@ class GlFieldErrors { ...@@ -50,11 +48,9 @@ class GlFieldErrors {
}); });
} }
focusOnFirstInvalid () { focusOnFirstInvalid() {
const firstInvalid = this.state.inputs.filter((input) => !input.inputDomElement.validity.valid)[0]; const firstInvalid = this.state.inputs
.filter(input => !input.inputDomElement.validity.valid)[0];
firstInvalid.inputElement.focus(); 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 DropzoneInput */
/* global autosize */ /* global autosize */
import GfmAutoComplete from './gfm_auto_complete'; import GfmAutoComplete from './gfm_auto_complete';
window.gl = window.gl || {}; export default class GLForm {
constructor(form, enableGFM = false) {
function GLForm(form, enableGFM = false) {
this.form = form; this.form = form;
this.textarea = this.form.find('textarea.js-gfm-input'); this.textarea = this.form.find('textarea.js-gfm-input');
this.enableGFM = enableGFM; this.enableGFM = enableGFM;
...@@ -16,20 +13,19 @@ function GLForm(form, enableGFM = false) { ...@@ -16,20 +13,19 @@ function GLForm(form, enableGFM = false) {
// Setup the form // Setup the form
this.setupForm(); this.setupForm();
this.form.data('gl-form', this); this.form.data('gl-form', this);
} }
GLForm.prototype.destroy = function() { destroy() {
// Clean form listeners // Clean form listeners
this.clearEventListeners(); this.clearEventListeners();
if (this.autoComplete) { if (this.autoComplete) {
this.autoComplete.destroy(); this.autoComplete.destroy();
} }
return this.form.data('gl-form', null); this.form.data('gl-form', null);
}; }
GLForm.prototype.setupForm = function() { setupForm() {
var isNewForm; const isNewForm = this.form.is(':not(.gfm-form)');
isNewForm = this.form.is(':not(.gfm-form)');
this.form.removeClass('js-new-note-form'); this.form.removeClass('js-new-note-form');
if (isNewForm) { if (isNewForm) {
this.form.find('.div-dropzone').remove(); this.form.find('.div-dropzone').remove();
...@@ -45,7 +41,7 @@ GLForm.prototype.setupForm = function() { ...@@ -45,7 +41,7 @@ GLForm.prototype.setupForm = function() {
mergeRequests: this.enableGFM, mergeRequests: this.enableGFM,
labels: this.enableGFM, labels: this.enableGFM,
}); });
new DropzoneInput(this.form); new DropzoneInput(this.form); // eslint-disable-line no-new
autosize(this.textarea); autosize(this.textarea);
} }
// form and textarea event listeners // form and textarea event listeners
...@@ -55,9 +51,9 @@ GLForm.prototype.setupForm = function() { ...@@ -55,9 +51,9 @@ GLForm.prototype.setupForm = function() {
this.form.find('.js-note-discard').hide(); this.form.find('.js-note-discard').hide();
this.form.show(); this.form.show();
if (this.isAutosizeable) this.setupAutosize(); if (this.isAutosizeable) this.setupAutosize();
}; }
GLForm.prototype.setupAutosize = function () { setupAutosize() {
this.textarea.off('autosize:resized') this.textarea.off('autosize:resized')
.on('autosize:resized', this.setHeightData.bind(this)); .on('autosize:resized', this.setHeightData.bind(this));
...@@ -68,13 +64,13 @@ GLForm.prototype.setupAutosize = function () { ...@@ -68,13 +64,13 @@ GLForm.prototype.setupAutosize = function () {
autosize(this.textarea); autosize(this.textarea);
this.textarea.css('resize', 'vertical'); this.textarea.css('resize', 'vertical');
}, 0); }, 0);
}; }
GLForm.prototype.setHeightData = function () { setHeightData() {
this.textarea.data('height', this.textarea.outerHeight()); this.textarea.data('height', this.textarea.outerHeight());
}; }
GLForm.prototype.destroyAutosize = function () { destroyAutosize() {
const outerHeight = this.textarea.outerHeight(); const outerHeight = this.textarea.outerHeight();
if (this.textarea.data('height') === outerHeight) return; if (this.textarea.data('height') === outerHeight) return;
...@@ -84,21 +80,20 @@ GLForm.prototype.destroyAutosize = function () { ...@@ -84,21 +80,20 @@ GLForm.prototype.destroyAutosize = function () {
this.textarea.data('height', outerHeight); this.textarea.data('height', outerHeight);
this.textarea.outerHeight(outerHeight); this.textarea.outerHeight(outerHeight);
this.textarea.css('max-height', window.outerHeight); this.textarea.css('max-height', window.outerHeight);
}; }
GLForm.prototype.clearEventListeners = function() { clearEventListeners() {
this.textarea.off('focus'); this.textarea.off('focus');
this.textarea.off('blur'); this.textarea.off('blur');
return gl.text.removeListeners(this.form); gl.text.removeListeners(this.form);
}; }
GLForm.prototype.addEventListeners = function() { addEventListeners() {
this.textarea.on('focus', function() { this.textarea.on('focus', function focusTextArea() {
return $(this).closest('.md-area').addClass('is-focused'); $(this).closest('.md-area').addClass('is-focused');
}); });
return this.textarea.on('blur', function() { this.textarea.on('blur', function blurTextArea() {
return $(this).closest('.md-area').removeClass('is-focused'); $(this).closest('.md-area').removeClass('is-focused');
}); });
}; }
}
window.gl.GLForm = GLForm;
...@@ -85,7 +85,7 @@ w.gl.utils.getLocationHash = function(url) { ...@@ -85,7 +85,7 @@ w.gl.utils.getLocationHash = function(url) {
return hashIndex === -1 ? null : url.substring(hashIndex + 1); 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 // eslint-disable-next-line import/prefer-default-export
export function visitUrl(url, external = false) { export function visitUrl(url, external = false) {
...@@ -96,7 +96,7 @@ export function visitUrl(url, external = false) { ...@@ -96,7 +96,7 @@ export function visitUrl(url, external = false) {
otherWindow.opener = null; otherWindow.opener = null;
otherWindow.location = url; otherWindow.location = url;
} else { } else {
document.location.href = url; window.location.href = url;
} }
} }
......
...@@ -44,7 +44,6 @@ import './aside'; ...@@ -44,7 +44,6 @@ import './aside';
import './autosave'; import './autosave';
import loadAwardsHandler from './awards_handler'; import loadAwardsHandler from './awards_handler';
import bp from './breakpoints'; import bp from './breakpoints';
import './broadcast_message';
import './commits'; import './commits';
import './compare'; import './compare';
import './compare_autocomplete'; import './compare_autocomplete';
...@@ -75,8 +74,6 @@ import './layout_nav'; ...@@ -75,8 +74,6 @@ import './layout_nav';
import LazyLoader from './lazy_loader'; import LazyLoader from './lazy_loader';
import './line_highlighter'; import './line_highlighter';
import './logo'; import './logo';
import './member_expiration_date';
import './members';
import './merge_request'; import './merge_request';
import './merge_request_tabs'; import './merge_request_tabs';
import './milestone'; import './milestone';
......
...@@ -2,14 +2,12 @@ ...@@ -2,14 +2,12 @@
import Pikaday from 'pikaday'; import Pikaday from 'pikaday';
(() => { // Add datepickers to all `js-access-expiration-date` elements. If those elements are
// 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
// 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
// `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.
// datepicker, and make clicking on that element clear the field. //
// export default function memberExpirationDate(selector = '.js-access-expiration-date') {
window.gl = window.gl || {};
gl.MemberExpirationDate = (selector = '.js-access-expiration-date') => {
function toggleClearInput() { function toggleClearInput() {
$(this).closest('.clearable-input').toggleClass('has-value', $(this).val() !== ''); $(this).closest('.clearable-input').toggleClass('has-value', $(this).val() !== '');
} }
...@@ -51,5 +49,4 @@ import Pikaday from 'pikaday'; ...@@ -51,5 +49,4 @@ import Pikaday from 'pikaday';
inputs.on('blur', toggleClearInput); inputs.on('blur', toggleClearInput);
inputs.each(toggleClearInput); inputs.each(toggleClearInput);
}; }
}).call(window);
/* eslint-disable class-methods-use-this, promise/catch-or-return */
/* eslint-disable no-new */
import Flash from './flash'; import Flash from './flash';
(() => { export default class Members {
window.gl = window.gl || {};
class Members {
constructor() { constructor() {
this.addListeners(); this.addListeners();
this.initGLDropdown(); this.initGLDropdown();
...@@ -64,7 +59,7 @@ import Flash from './flash'; ...@@ -64,7 +59,7 @@ import Flash from './flash';
}); });
}); });
} }
// eslint-disable-next-line class-methods-use-this
removeRow(e) { removeRow(e) {
const $target = $(e.target); const $target = $(e.target);
...@@ -100,7 +95,7 @@ import Flash from './flash'; ...@@ -100,7 +95,7 @@ import Flash from './flash';
$ldapPermissionsElement.toggle(); $ldapPermissionsElement.toggle();
} }
// eslint-disable-next-line class-methods-use-this
getMemberListItems($el) { getMemberListItems($el) {
const $memberListItem = $el.is('.member') ? $el : $(`#${$el.data('el-id')}`); const $memberListItem = $el.is('.member') ? $el : $(`#${$el.data('el-id')}`);
...@@ -116,7 +111,7 @@ import Flash from './flash'; ...@@ -116,7 +111,7 @@ import Flash from './flash';
const { $memberListItem, $toggle, $dateInput } = this.getMemberListItems($btn); const { $memberListItem, $toggle, $dateInput } = this.getMemberListItems($btn);
$btn.disable(); $btn.disable();
// eslint-disable-next-line promise/catch-or-return
this.overrideLdap($memberListItem, $btn.data('endpoint'), true).then(() => { this.overrideLdap($memberListItem, $btn.data('endpoint'), true).then(() => {
this.showLDAPPermissionsWarning(e); this.showLDAPPermissionsWarning(e);
...@@ -126,13 +121,13 @@ import Flash from './flash'; ...@@ -126,13 +121,13 @@ import Flash from './flash';
$btn.enable(); $btn.enable();
if (xhr.status === 403) { if (xhr.status === 403) {
new Flash('You do not have the correct permissions to override the settings from the LDAP group sync.', 'alert'); Flash('You do not have the correct permissions to override the settings from the LDAP group sync.', 'alert');
} else { } else {
new Flash('An error occured whilst saving LDAP override status. Please try again.', 'alert'); 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) { overrideLdap($memberListitem, endpoint, override) {
return $.ajax({ return $.ajax({
url: endpoint, url: endpoint,
...@@ -146,7 +141,4 @@ import Flash from './flash'; ...@@ -146,7 +141,4 @@ import Flash from './flash';
$memberListitem.toggleClass('is-overriden', override); $memberListitem.toggleClass('is-overriden', override);
}); });
} }
} }
gl.Members = Members;
})();
...@@ -19,6 +19,7 @@ import 'vendor/jquery.atwho'; ...@@ -19,6 +19,7 @@ import 'vendor/jquery.atwho';
import AjaxCache from '~/lib/utils/ajax_cache'; import AjaxCache from '~/lib/utils/ajax_cache';
import Flash from './flash'; import Flash from './flash';
import CommentTypeToggle from './comment_type_toggle'; import CommentTypeToggle from './comment_type_toggle';
import GLForm from './gl_form';
import loadAwardsHandler from './awards_handler'; import loadAwardsHandler from './awards_handler';
import './autosave'; import './autosave';
import './dropzone_input'; import './dropzone_input';
...@@ -557,7 +558,7 @@ export default class Notes { ...@@ -557,7 +558,7 @@ export default class Notes {
*/ */
setupNoteForm(form) { setupNoteForm(form) {
var textarea, key; var textarea, key;
new gl.GLForm(form, this.enableGFM); this.glForm = new GLForm(form, this.enableGFM);
textarea = form.find('.js-note-text'); textarea = form.find('.js-note-text');
key = [ key = [
'Note', 'Note',
...@@ -1152,7 +1153,7 @@ export default class Notes { ...@@ -1152,7 +1153,7 @@ export default class Notes {
var targetId = $originalContentEl.data('target-id'); var targetId = $originalContentEl.data('target-id');
var targetType = $originalContentEl.data('target-type'); 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') $editForm.find('form')
.attr('action', postUrl) .attr('action', postUrl)
......
import Vue from 'vue'; import Vue from 'vue';
import Translate from '../vue_shared/translate'; import Translate from '../vue_shared/translate';
import GlFieldErrors from '../gl_field_errors';
import intervalPatternInput from './components/interval_pattern_input.vue'; import intervalPatternInput from './components/interval_pattern_input.vue';
import TimezoneDropdown from './components/timezone_dropdown'; import TimezoneDropdown from './components/timezone_dropdown';
import TargetBranchDropdown from './components/target_branch_dropdown'; import TargetBranchDropdown from './components/target_branch_dropdown';
...@@ -39,7 +40,7 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -39,7 +40,7 @@ document.addEventListener('DOMContentLoaded', () => {
gl.timezoneDropdown = new TimezoneDropdown(); gl.timezoneDropdown = new TimezoneDropdown();
gl.targetBranchDropdown = new TargetBranchDropdown(); gl.targetBranchDropdown = new TargetBranchDropdown();
gl.pipelineScheduleFieldErrors = new gl.GlFieldErrors(formElement); gl.pipelineScheduleFieldErrors = new GlFieldErrors(formElement);
setupPipelineVariableList($('.js-pipeline-variable-list')); setupPipelineVariableList($('.js-pipeline-variable-list'));
}); });
...@@ -81,7 +81,11 @@ export default class PrometheusMetrics { ...@@ -81,7 +81,11 @@ export default class PrometheusMetrics {
loadActiveMetrics() { loadActiveMetrics() {
this.showMonitoringMetricsPanelState(PANEL_STATE.LOADING); this.showMonitoringMetricsPanelState(PANEL_STATE.LOADING);
backOff((next, stop) => { backOff((next, stop) => {
$.getJSON(this.activeMetricsEndpoint) $.ajax({
url: this.activeMetricsEndpoint,
dataType: 'json',
global: false,
})
.done((res) => { .done((res) => {
if (res && res.success) { if (res && res.success) {
stop(res); stop(res);
......
...@@ -3,11 +3,17 @@ import Flash from '../../flash'; ...@@ -3,11 +3,17 @@ import Flash from '../../flash';
import Store from '../stores/repo_store'; import Store from '../stores/repo_store';
import RepoMixin from '../mixins/repo_mixin'; import RepoMixin from '../mixins/repo_mixin';
import Service from '../services/repo_service'; import Service from '../services/repo_service';
import PopupDialog from '../../vue_shared/components/popup_dialog.vue';
import { visitUrl } from '../../lib/utils/url_utility';
export default { export default {
mixins: [RepoMixin],
data: () => Store, data: () => Store,
mixins: [RepoMixin], components: {
PopupDialog,
},
computed: { computed: {
showCommitable() { showCommitable() {
...@@ -28,7 +34,16 @@ export default { ...@@ -28,7 +34,16 @@ export default {
}, },
methods: { 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 // see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions
const commitMessage = this.commitMessage; const commitMessage = this.commitMessage;
const actions = this.changedFiles.map(f => ({ const actions = this.changedFiles.map(f => ({
...@@ -36,19 +51,63 @@ export default { ...@@ -36,19 +51,63 @@ export default {
file_path: f.path, file_path: f.path,
content: f.newContent, content: f.newContent,
})); }));
const branch = newBranch ? `${this.currentBranch}-${this.currentShortHash}` : this.currentBranch;
const payload = { const payload = {
branch: Store.currentBranch, branch,
commit_message: commitMessage, commit_message: commitMessage,
actions, actions,
}; };
Store.submitCommitsLoading = true; if (newBranch) {
payload.start_branch = this.currentBranch;
}
this.submitCommitsLoading = true;
Service.commitFiles(payload) Service.commitFiles(payload)
.then(this.resetCommitState) .then(() => {
.catch(() => Flash('An error occurred while committing your changes')); 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() { resetCommitState() {
this.submitCommitsLoading = false; this.submitCommitsLoading = false;
this.openedFiles = this.openedFiles.map((file) => {
const f = file;
f.changed = false;
return f;
});
this.changedFiles = []; this.changedFiles = [];
this.commitMessage = ''; this.commitMessage = '';
this.editMode = false; this.editMode = false;
...@@ -62,9 +121,17 @@ export default { ...@@ -62,9 +121,17 @@ export default {
<div <div
v-if="showCommitable" v-if="showCommitable"
id="commit-area"> 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 <form
class="form-horizontal" class="form-horizontal"
@submit.prevent="makeCommit"> @submit.prevent="tryCommit">
<fieldset> <fieldset>
<div class="form-group"> <div class="form-group">
<label class="col-md-4 control-label staged-files"> <label class="col-md-4 control-label staged-files">
...@@ -117,7 +184,7 @@ export default { ...@@ -117,7 +184,7 @@ export default {
class="btn btn-success"> class="btn btn-success">
<i <i
v-if="submitCommitsLoading" v-if="submitCommitsLoading"
class="fa fa-spinner fa-spin" class="js-commit-loading-icon fa fa-spinner fa-spin"
aria-hidden="true" aria-hidden="true"
aria-label="loading"> aria-label="loading">
</i> </i>
...@@ -126,6 +193,14 @@ export default { ...@@ -126,6 +193,14 @@ export default {
</span> </span>
</button> </button>
</div> </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> </fieldset>
</form> </form>
</div> </div>
......
...@@ -31,8 +31,11 @@ function setInitialStore(data) { ...@@ -31,8 +31,11 @@ function setInitialStore(data) {
Store.projectUrl = data.projectUrl; Store.projectUrl = data.projectUrl;
Store.canCommit = data.canCommit; Store.canCommit = data.canCommit;
Store.onTopOfBranch = data.onTopOfBranch; Store.onTopOfBranch = data.onTopOfBranch;
Store.newMrTemplateUrl = decodeURIComponent(data.newMrTemplateUrl);
Store.customBranchURL = decodeURIComponent(data.blobUrl);
Store.currentBranch = $('button.dropdown-menu-toggle').attr('data-ref'); Store.currentBranch = $('button.dropdown-menu-toggle').attr('data-ref');
Store.checkIsCommitable(); Store.checkIsCommitable();
Store.setBranchHash();
} }
function initRepo(el) { function initRepo(el) {
......
...@@ -64,6 +64,10 @@ const RepoService = { ...@@ -64,6 +64,10 @@ const RepoService = {
return urlArray.join('/'); return urlArray.join('/');
}, },
getBranch() {
return Api.branchSingle(Store.projectId, Store.currentBranch);
},
commitFiles(payload) { commitFiles(payload) {
return Api.commitMultiple(Store.projectId, payload) return Api.commitMultiple(Store.projectId, payload)
.then(this.commitFlash); .then(this.commitFlash);
......
...@@ -23,6 +23,7 @@ const RepoStore = { ...@@ -23,6 +23,7 @@ const RepoStore = {
title: '', title: '',
status: false, status: false,
}, },
showNewBranchDialog: false,
activeFile: Helper.getDefaultActiveFile(), activeFile: Helper.getDefaultActiveFile(),
activeFileIndex: 0, activeFileIndex: 0,
activeLine: -1, activeLine: -1,
...@@ -31,6 +32,12 @@ const RepoStore = { ...@@ -31,6 +32,12 @@ const RepoStore = {
isCommitable: false, isCommitable: false,
binary: false, binary: false,
currentBranch: '', currentBranch: '',
startNewMR: false,
currentHash: '',
currentShortHash: '',
customBranchURL: '',
newMrTemplateUrl: '',
branchChanged: false,
commitMessage: '', commitMessage: '',
binaryTypes: { binaryTypes: {
png: false, png: false,
...@@ -49,6 +56,17 @@ const RepoStore = { ...@@ -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 // mutations
checkIsCommitable() { checkIsCommitable() {
RepoStore.isCommitable = RepoStore.onTopOfBranch && RepoStore.canCommit; RepoStore.isCommitable = RepoStore.onTopOfBranch && RepoStore.canCommit;
......
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import UserTabs from './user_tabs'; import UserTabs from './user_tabs';
export default function initUserProfile(action) { function initUserProfile(action) {
// place profile avatars to top // place profile avatars to top
$('.profile-groups-avatars').tooltip({ $('.profile-groups-avatars').tooltip({
placement: 'top', placement: 'top',
...@@ -17,3 +17,9 @@ export default function initUserProfile(action) { ...@@ -17,3 +17,9 @@ export default function initUserProfile(action) {
$(this).parents('.project-limit-message').remove(); $(this).parents('.project-limit-message').remove();
}); });
} }
document.addEventListener('DOMContentLoaded', () => {
const page = $('body').attr('data-page');
const action = page.split(':')[1];
initUserProfile(action);
});
<script> <script>
import Flash from '../../../flash'; import Flash from '../../../flash';
import GLForm from '../../../gl_form';
import markdownHeader from './header.vue'; import markdownHeader from './header.vue';
import markdownToolbar from './toolbar.vue'; import markdownToolbar from './toolbar.vue';
...@@ -85,7 +86,7 @@ ...@@ -85,7 +86,7 @@
/* /*
GLForm class handles all the toolbar buttons 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() { beforeDestroy() {
const glForm = $(this.$refs['gl-form']).data('gl-form'); const glForm = $(this.$refs['gl-form']).data('gl-form');
......
...@@ -16,6 +16,11 @@ export default { ...@@ -16,6 +16,11 @@ export default {
required: false, required: false,
default: 'primary', default: 'primary',
}, },
closeKind: {
type: String,
required: false,
default: 'default',
},
closeButtonLabel: { closeButtonLabel: {
type: String, type: String,
required: false, required: false,
...@@ -33,6 +38,11 @@ export default { ...@@ -33,6 +38,11 @@ export default {
[`btn-${this.kind}`]: true, [`btn-${this.kind}`]: true,
}; };
}, },
btnCancelKindClass() {
return {
[`btn-${this.closeKind}`]: true,
};
},
}, },
methods: { methods: {
...@@ -70,7 +80,8 @@ export default { ...@@ -70,7 +80,8 @@ export default {
<div class="modal-footer"> <div class="modal-footer">
<button <button
type="button" type="button"
class="btn btn-default" class="btn"
:class="btnCancelKindClass"
@click="emitSubmit(false)"> @click="emitSubmit(false)">
{{closeButtonLabel}} {{closeButtonLabel}}
</button> </button>
......
...@@ -90,7 +90,7 @@ $new-sidebar-collapsed-width: 50px; ...@@ -90,7 +90,7 @@ $new-sidebar-collapsed-width: 50px;
top: $header-height; top: $header-height;
bottom: 0; bottom: 0;
left: 0; left: 0;
background-color: $gray-normal; background-color: $gray-light;
box-shadow: inset -2px 0 0 $border-color; box-shadow: inset -2px 0 0 $border-color;
transform: translate3d(0, 0, 0); transform: translate3d(0, 0, 0);
......
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
margin: 0; margin: 0;
list-style: none; list-style: none;
height: auto; height: auto;
border-bottom: 1px solid $border-color;
li { li {
display: flex; display: flex;
...@@ -24,6 +25,7 @@ ...@@ -24,6 +25,7 @@
&:focus { &:focus {
text-decoration: none; text-decoration: none;
color: $black; color: $black;
border-bottom: 2px solid $gray-darkest;
.badge { .badge {
color: $black; color: $black;
......
...@@ -707,11 +707,11 @@ ...@@ -707,11 +707,11 @@
.frame.click-to-comment { .frame.click-to-comment {
position: relative; 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; $image-comment-cursor-left-offset $image-comment-cursor-top-offset, auto;
// Retina cursor // 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; $image-comment-cursor-left-offset $image-comment-cursor-top-offset, auto;
.comment-indicator { .comment-indicator {
......
...@@ -653,10 +653,6 @@ a.deploy-project-label { ...@@ -653,10 +653,6 @@ a.deploy-project-label {
} }
.project-import { .project-import {
.form-group {
margin-bottom: 0;
}
.import-btn-container { .import-btn-container {
margin-bottom: 0; margin-bottom: 0;
} }
......
...@@ -315,20 +315,12 @@ module IssuablesHelper ...@@ -315,20 +315,12 @@ module IssuablesHelper
@issuable_templates ||= @issuable_templates ||=
case issuable case issuable
when Issue when Issue
issue_template_names ref_project.repository.issue_template_names
when MergeRequest when MergeRequest
merge_request_template_names ref_project.repository.merge_request_template_names
end end
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) def selected_template(issuable)
params[:issuable_template] if issuable_templates(issuable).any? { |template| template[:name] == params[:issuable_template] } params[:issuable_template] if issuable_templates(issuable).any? { |template| template[:name] == params[:issuable_template] }
end end
......
...@@ -156,7 +156,9 @@ class Blob < SimpleDelegator ...@@ -156,7 +156,9 @@ class Blob < SimpleDelegator
end end
def file_type def file_type
Gitlab::FileDetector.type_of(path) name = File.basename(path)
Gitlab::FileDetector.type_of(path) || Gitlab::FileDetector.type_of(name)
end end
def video? def video?
......
class OauthAccessToken < Doorkeeper::AccessToken class OauthAccessToken < Doorkeeper::AccessToken
belongs_to :resource_owner, class_name: 'User' belongs_to :resource_owner, class_name: 'User'
belongs_to :application, class_name: 'Doorkeeper::Application' belongs_to :application, class_name: 'Doorkeeper::Application'
alias_method :user, :resource_owner
end end
...@@ -41,7 +41,8 @@ class Repository ...@@ -41,7 +41,8 @@ class Repository
CACHED_METHODS = %i(size commit_count rendered_readme contribution_guide CACHED_METHODS = %i(size commit_count rendered_readme contribution_guide
changelog license_blob license_key gitignore koding_yml changelog license_blob license_key gitignore koding_yml
gitlab_ci_yml branch_names tag_names branch_count 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 # Methods that use cache_method but only memoize the value
MEMOIZED_CACHED_METHODS = %i(license empty_repo?).freeze MEMOIZED_CACHED_METHODS = %i(license empty_repo?).freeze
...@@ -57,7 +58,9 @@ class Repository ...@@ -57,7 +58,9 @@ class Repository
gitignore: :gitignore, gitignore: :gitignore,
koding: :koding_yml, koding: :koding_yml,
gitlab_ci: :gitlab_ci_yml, gitlab_ci: :gitlab_ci_yml,
avatar: :avatar avatar: :avatar,
issue_template: :issue_template_names,
merge_request_template: :merge_request_template_names
}.freeze }.freeze
# Wraps around the given method and caches its output in Redis and an instance # Wraps around the given method and caches its output in Redis and an instance
...@@ -540,6 +543,16 @@ class Repository ...@@ -540,6 +543,16 @@ class Repository
end end
cache_method :avatar 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 def readme
if readme = tree(:head)&.readme if readme = tree(:head)&.readme
ReadmeBlob.new(readme, self) ReadmeBlob.new(readme, self)
......
...@@ -56,11 +56,22 @@ module Auth ...@@ -56,11 +56,22 @@ module Auth
def process_scope(scope) def process_scope(scope)
type, name, actions = scope.split(':', 3) type, name, actions = scope.split(':', 3)
actions = actions.split(',') actions = actions.split(',')
case type
when 'registry'
process_registry_access(type, name, actions)
when 'repository'
path = ContainerRegistry::Path.new(name) path = ContainerRegistry::Path.new(name)
process_repository_access(type, path, actions)
end
end
return unless type == 'repository' 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 end
def process_repository_access(type, path, actions) def process_repository_access(type, path, actions)
......
...@@ -84,15 +84,11 @@ module MergeRequests ...@@ -84,15 +84,11 @@ module MergeRequests
def after_merge def after_merge
MergeRequests::PostMergeService.new(project, current_user).execute(merge_request) MergeRequests::PostMergeService.new(project, current_user).execute(merge_request)
if params[:should_remove_source_branch].present? || @merge_request.force_remove_source_branch? if delete_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) DeleteBranchService.new(@merge_request.source_project, branch_deletion_user)
.execute(merge_request.source_branch) .execute(merge_request.source_branch)
end end
end end
end
def clean_merge_jid def clean_merge_jid
merge_request.update_column(:merge_jid, nil) merge_request.update_column(:merge_jid, nil)
...@@ -102,6 +98,14 @@ module MergeRequests ...@@ -102,6 +98,14 @@ module MergeRequests
@merge_request.force_remove_source_branch? ? @merge_request.author : current_user @merge_request.force_remove_source_branch? ? @merge_request.author : current_user
end 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`. # Logs merge error message and cleans `MergeRequest#merge_jid`.
# #
def handle_merge_error(log_message:, save_message_on_model: false) def handle_merge_error(log_message:, save_message_on_model: false)
......
...@@ -413,7 +413,7 @@ class NotificationService ...@@ -413,7 +413,7 @@ class NotificationService
end end
def relabeled_resource_email(target, labels, current_user, method) 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 = notifiable_users(
recipients, :subscription, recipients, :subscription,
target: target, target: target,
......
...@@ -31,7 +31,7 @@ ...@@ -31,7 +31,7 @@
%tbody %tbody
%tr %tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } %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;" } %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;" } %a.muted{ href: commits_url(@pipeline), style: "color:#333333;text-decoration:none;" }
= @pipeline.ref = @pipeline.ref
...@@ -42,7 +42,7 @@ ...@@ -42,7 +42,7 @@
%tbody %tbody
%tr %tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } %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;" } %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;" } %a{ href: commit_url(@pipeline), style: "color:#3777b0;text-decoration:none;" }
= @pipeline.short_sha = @pipeline.short_sha
...@@ -60,7 +60,7 @@ ...@@ -60,7 +60,7 @@
%tbody %tbody
%tr %tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } %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;" } %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
- if commit.author - if commit.author
%a.muted{ href: user_url(commit.author), style: "color:#333333;text-decoration:none;" } %a.muted{ href: user_url(commit.author), style: "color:#333333;text-decoration:none;" }
...@@ -76,7 +76,7 @@ ...@@ -76,7 +76,7 @@
%tbody %tbody
%tr %tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } %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;" } %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
- if commit.committer - if commit.committer
%a.muted{ href: user_url(commit.committer), style: "color:#333333;text-decoration:none;" } %a.muted{ href: user_url(commit.committer), style: "color:#333333;text-decoration:none;" }
...@@ -100,7 +100,7 @@ ...@@ -100,7 +100,7 @@
triggered by triggered by
- if @pipeline.user - 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" } %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;" } %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;" } %a.muted{ href: user_url(@pipeline.user), style: "color:#333333;text-decoration:none;" }
= @pipeline.user.name = @pipeline.user.name
......
...@@ -31,7 +31,7 @@ ...@@ -31,7 +31,7 @@
%tbody %tbody
%tr %tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } %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;" } %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;" } %a.muted{ href: commits_url(@pipeline), style: "color:#333333;text-decoration:none;" }
= @pipeline.ref = @pipeline.ref
...@@ -42,7 +42,7 @@ ...@@ -42,7 +42,7 @@
%tbody %tbody
%tr %tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } %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;" } %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;" } %a{ href: commit_url(@pipeline), style: "color:#3777b0;text-decoration:none;" }
= @pipeline.short_sha = @pipeline.short_sha
...@@ -60,7 +60,7 @@ ...@@ -60,7 +60,7 @@
%tbody %tbody
%tr %tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } %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;" } %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
- if commit.author - if commit.author
%a.muted{ href: user_url(commit.author), style: "color:#333333;text-decoration:none;" } %a.muted{ href: user_url(commit.author), style: "color:#333333;text-decoration:none;" }
...@@ -76,7 +76,7 @@ ...@@ -76,7 +76,7 @@
%tbody %tbody
%tr %tr
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" } %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;" } %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
- if commit.committer - if commit.committer
%a.muted{ href: user_url(commit.committer), style: "color:#333333;text-decoration:none;" } %a.muted{ href: user_url(commit.committer), style: "color:#333333;text-decoration:none;" }
...@@ -100,7 +100,7 @@ ...@@ -100,7 +100,7 @@
triggered by triggered by
- if @pipeline.user - 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" } %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;" } %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;" } %a.muted{ href: user_url(@pipeline.user), style: "color:#333333;text-decoration:none;" }
= @pipeline.user.name = @pipeline.user.name
......
...@@ -3,5 +3,7 @@ ...@@ -3,5 +3,7 @@
refs_url: refs_project_path(project, format: :json), refs_url: refs_project_path(project, format: :json),
project_url: project_path(project), project_url: project_path(project),
project_id: project.id, 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, can_commit: (!!can_push_branch?(project, @ref)).to_s,
on_top_of_branch: (!!on_top_of_branch?(project, @ref)).to_s } } on_top_of_branch: (!!on_top_of_branch?(project, @ref)).to_s } }
...@@ -4,6 +4,9 @@ ...@@ -4,6 +4,9 @@
- page_description @user.bio - page_description @user.bio
- header_title @user.name, user_path(@user) - header_title @user.name, user_path(@user)
- @no_container = true - @no_container = true
- content_for :page_specific_javascripts do
= webpack_bundle_tag 'common_d3'
= webpack_bundle_tag 'users'
= content_for :meta_tags do = content_for :meta_tags do
= auto_discovery_link_tag(:atom, user_url(@user, format: :atom), title: "#{@user.name} activity") = 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 = { ...@@ -93,6 +93,7 @@ var config = {
vue_merge_request_widget: './vue_merge_request_widget/index.js', vue_merge_request_widget: './vue_merge_request_widget/index.js',
test: './test.js', test: './test.js',
two_factor_auth: './two_factor_auth.js', two_factor_auth: './two_factor_auth.js',
users: './users/index.js',
performance_bar: './performance_bar.js', performance_bar: './performance_bar.js',
webpack_runtime: './webpack.js', webpack_runtime: './webpack.js',
}, },
...@@ -226,8 +227,10 @@ var config = { ...@@ -226,8 +227,10 @@ var config = {
name: 'common_d3', name: 'common_d3',
chunks: [ chunks: [
'graphs', 'graphs',
'graphs_show',
'monitoring', 'monitoring',
'burndown_chart', 'users',
'burndown_chart', // EE
], ],
}), }),
......
...@@ -58,6 +58,18 @@ For example, in French we translate `you` as the informal `tu`. ...@@ -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 You can refer to other translated strings and notes in the glossary to assist determining a
suitable level of formality. 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 ### Updating the glossary
To propose additions to the glossary please 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 ...@@ -27,3 +27,13 @@ Bullet will log query problems to both the Rails log as well as the Chrome
console. console.
As a follow up to finding `N+1` queries with Bullet, consider writing a [QueryRecoder test](query_recorder.md) to prevent a regression. 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>
...@@ -299,9 +299,9 @@ sudo usermod -aG redis git ...@@ -299,9 +299,9 @@ sudo usermod -aG redis git
### Clone the Source ### Clone the Source
# Clone GitLab repository # 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 ### Configure It
......
...@@ -150,7 +150,7 @@ sudo -u git -H make ...@@ -150,7 +150,7 @@ sudo -u git -H make
#### New Gitaly configuration options required #### 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 ```shell
echo ' echo '
...@@ -335,11 +335,11 @@ sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production ...@@ -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! 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 ### 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). database migration (the backup is already migrated to the previous version).
### 2. Restore from the backup ### 2. Restore from the backup
......
...@@ -155,7 +155,7 @@ comments in greater detail. ...@@ -155,7 +155,7 @@ comments in greater detail.
## Image discussions ## 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, 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 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 ...@@ -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-7180]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7180
[ce-8266]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8266 [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-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 [ce-14531]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14531
[resolve-discussion-button]: img/resolve_discussion_button.png [resolve-discussion-button]: img/resolve_discussion_button.png
[resolve-comment-button]: img/resolve_comment_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 ...@@ -478,19 +478,6 @@ module SharedPaths
# ---------------------------------------- # ----------------------------------------
# Public Projects # 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 step 'I visit the public groups area' do
visit explore_groups_path visit explore_groups_path
end end
......
...@@ -112,10 +112,6 @@ module SharedProject ...@@ -112,10 +112,6 @@ module SharedProject
# Visibility of archived project # 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 step 'I should not see project "Archive"' do
project = Project.find_by(name: "Archive") project = Project.find_by(name: "Archive")
expect(page).not_to have_content project.name_with_namespace expect(page).not_to have_content project.name_with_namespace
...@@ -126,11 +122,6 @@ module SharedProject ...@@ -126,11 +122,6 @@ module SharedProject
expect(page).to have_content project.name_with_namespace expect(page).to have_content project.name_with_namespace
end end
step 'project "Archive" has comments' do
project = Project.find_by(name: "Archive")
2.times { create(:note_on_issue, project: project) }
end
# ---------------------------------------- # ----------------------------------------
# Visibility level # Visibility level
# ---------------------------------------- # ----------------------------------------
...@@ -209,15 +200,6 @@ module SharedProject ...@@ -209,15 +200,6 @@ module SharedProject
create :project_empty_repo, :public, name: "Empty Public Project" create :project_empty_repo, :public, name: "Empty Public Project"
end 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 step 'project "Shop" has labels: "bug", "feature", "enhancement"' do
project = Project.find_by(name: "Shop") project = Project.find_by(name: "Shop")
create(:label, project: project, title: 'bug') create(:label, project: project, title: 'bug')
......
...@@ -44,6 +44,39 @@ module API ...@@ -44,6 +44,39 @@ module API
# Helper Methods for Grape Endpoint # Helper Methods for Grape Endpoint
module HelperMethods 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. # Invokes the doorkeeper guard.
# #
# If token is presented and valid, then it sets @current_user. # If token is presented and valid, then it sets @current_user.
...@@ -62,37 +95,36 @@ module API ...@@ -62,37 +95,36 @@ module API
# scopes: (optional) scopes required for this guard. # scopes: (optional) scopes required for this guard.
# Defaults to empty array. # Defaults to empty array.
# #
def doorkeeper_guard(scopes: []) def find_user_from_oauth_token
access_token = find_access_token access_token = find_oauth_access_token
return nil unless access_token return 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
when AccessTokenValidationService::REVOKED
raise RevokedError
when AccessTokenValidationService::VALID find_user_by_access_token(access_token)
User.find(access_token.resource_owner_id)
end end
def find_user_by_authentication_token(token_string)
User.find_by_authentication_token(token_string)
end end
def find_user_by_private_token(scopes: []) def find_user_by_personal_access_token(token_string)
token_string = (params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER]).to_s 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 = # Check the Rails session for valid authentication details
find_user_by_authentication_token(token_string) || def find_user_from_warden
find_user_by_personal_access_token(token_string, scopes) 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 end
def find_user_by_job_token def find_user_by_job_token
...@@ -105,43 +137,63 @@ module API ...@@ -105,43 +137,63 @@ module API
end end
end end
private
def route_authentication_setting def route_authentication_setting
return {} unless respond_to?(:route_setting) return {} unless respond_to?(:route_setting)
route_setting(:authentication) || {} route_setting(:authentication) || {}
end end
def find_user_by_authentication_token(token_string) def find_oauth_access_token
User.find_by_authentication_token(token_string) return @oauth_access_token if defined?(@oauth_access_token)
end
def find_user_by_personal_access_token(token_string, scopes) token = Doorkeeper::OAuth::Token.from_request(doorkeeper_request, *Doorkeeper.configuration.access_token_methods)
access_token = PersonalAccessToken.active.find_by_token(token_string) return @oauth_access_token = nil unless token
return unless access_token
if AccessTokenValidationService.new(access_token, request: request).include_any_scope?(scopes) @oauth_access_token = OauthAccessToken.by_token(token)
User.find(access_token.user_id) raise UnauthorizedError unless @oauth_access_token
end
@oauth_access_token.revoke_previous_refresh_token!
@oauth_access_token
end end
def find_access_token def find_user_by_access_token(access_token)
return @access_token if defined?(@access_token) scopes = scopes_registered_for_endpoint
token = Doorkeeper::OAuth::Token.from_request(doorkeeper_request, *Doorkeeper.configuration.access_token_methods) case AccessTokenValidationService.new(access_token, request: request).validate(scopes: scopes)
return @access_token = nil unless token when AccessTokenValidationService::INSUFFICIENT_SCOPE
raise InsufficientScopeError.new(scopes)
@access_token = Doorkeeper::AccessToken.by_token(token) when AccessTokenValidationService::EXPIRED
raise UnauthorizedError unless @access_token raise ExpiredError
@access_token.revoke_previous_refresh_token! when AccessTokenValidationService::REVOKED
@access_token raise RevokedError
when AccessTokenValidationService::VALID
access_token.user
end
end end
def doorkeeper_request def doorkeeper_request
@doorkeeper_request ||= ActionDispatch::Request.new(env) @doorkeeper_request ||= ActionDispatch::Request.new(env)
end 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
module ClassMethods module ClassMethods
......
...@@ -5,8 +5,6 @@ module API ...@@ -5,8 +5,6 @@ module API
include Gitlab::Utils include Gitlab::Utils
include Helpers::Pagination include Helpers::Pagination
UnauthorizedError = Class.new(StandardError)
SUDO_HEADER = "HTTP_SUDO".freeze SUDO_HEADER = "HTTP_SUDO".freeze
SUDO_PARAM = :sudo SUDO_PARAM = :sudo
...@@ -426,25 +424,11 @@ module API ...@@ -426,25 +424,11 @@ module API
begin begin
@initial_current_user = Gitlab::Auth::UniqueIpsLimiter.limit_user! { find_current_user } @initial_current_user = Gitlab::Auth::UniqueIpsLimiter.limit_user! { find_current_user }
rescue APIGuard::UnauthorizedError, UnauthorizedError rescue APIGuard::UnauthorizedError
unauthorized! unauthorized!
end end
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! def sudo!
return unless sudo_identifier return unless sudo_identifier
return unless initial_current_user return unless initial_current_user
...@@ -508,22 +492,5 @@ module API ...@@ -508,22 +492,5 @@ module API
exception.status == 500 exception.status == 500
end 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
end end
...@@ -6,31 +6,33 @@ module Gitlab ...@@ -6,31 +6,33 @@ module Gitlab
module FileDetector module FileDetector
PATTERNS = { PATTERNS = {
# Project files # Project files
readme: /\Areadme/i, readme: /\Areadme[^\/]*\z/i,
changelog: /\A(changelog|history|changes|news)/i, changelog: /\A(changelog|history|changes|news)[^\/]*\z/i,
license: /\A(licen[sc]e|copying)(\..+|\z)/i, license: /\A(licen[sc]e|copying)(\.[^\/]+)?\z/i,
contributing: /\Acontributing/i, contributing: /\Acontributing[^\/]*\z/i,
version: 'version', version: 'version',
avatar: /\Alogo\.(png|jpg|gif)\z/, 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 # Configuration files
gitignore: '.gitignore', gitignore: '.gitignore',
koding: '.koding.yml', koding: '.koding.yml',
gitlab_ci: '.gitlab-ci.yml', gitlab_ci: '.gitlab-ci.yml',
route_map: 'route-map.yml', route_map: '.gitlab/route-map.yml',
# Dependency files # Dependency files
cartfile: /\ACartfile/, cartfile: /\ACartfile[^\/]*\z/,
composer_json: 'composer.json', composer_json: 'composer.json',
gemfile: /\A(Gemfile|gems\.rb)\z/, gemfile: /\A(Gemfile|gems\.rb)\z/,
gemfile_lock: 'Gemfile.lock', gemfile_lock: 'Gemfile.lock',
gemspec: /\.gemspec\z/, gemspec: /\A[^\/]*\.gemspec\z/,
godeps_json: 'Godeps.json', godeps_json: 'Godeps.json',
package_json: 'package.json', package_json: 'package.json',
podfile: 'Podfile', podfile: 'Podfile',
podspec_json: /\.podspec\.json\z/, podspec_json: /\A[^\/]*\.podspec\.json\z/,
podspec: /\.podspec\z/, podspec: /\A[^\/]*\.podspec\z/,
requirements_txt: /requirements\.txt\z/, requirements_txt: /\A[^\/]*requirements\.txt\z/,
yarn_lock: 'yarn.lock' yarn_lock: 'yarn.lock'
}.freeze }.freeze
...@@ -63,13 +65,11 @@ module Gitlab ...@@ -63,13 +65,11 @@ module Gitlab
# type_of('README.md') # => :readme # type_of('README.md') # => :readme
# type_of('VERSION') # => :version # type_of('VERSION') # => :version
def self.type_of(path) def self.type_of(path)
name = File.basename(path)
PATTERNS.each do |type, search| PATTERNS.each do |type, search|
did_match = if search.is_a?(Regexp) did_match = if search.is_a?(Regexp)
name =~ search path =~ search
else else
name.casecmp(search) == 0 path.casecmp(search) == 0
end end
return type if did_match return type if did_match
......
...@@ -79,6 +79,12 @@ FactoryGirl.define do ...@@ -79,6 +79,12 @@ FactoryGirl.define do
merge_user author merge_user author
end end
trait :remove_source_branch do
merge_params do
{ 'force_remove_source_branch' => '1' }
end
end
after(:build) do |merge_request| after(:build) do |merge_request|
target_project = merge_request.target_project target_project = merge_request.target_project
source_project = merge_request.source_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' require 'spec_helper'
describe 'User views open merge requests' do describe 'User views open merge requests' do
let(:project) { create(:project, :public, :repository) } set(:user) { create(:user) }
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
context 'when project is public' do
set(:project) { create(:project, :public, :repository) }
context 'when not signed in' do
context "when the target branch is the project's default branch" 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!(: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) } let!(:closed_merge_request) { create(:closed_merge_request, source_project: project, target_project: project) }
...@@ -11,6 +21,8 @@ describe 'User views open merge requests' do ...@@ -11,6 +21,8 @@ describe 'User views open merge requests' do
visit(project_merge_requests_path(project)) visit(project_merge_requests_path(project))
end end
include_examples 'shows merge requests'
it 'shows open merge requests' do it 'shows open merge requests' do
expect(page).to have_content(merge_request.title).and have_no_content(closed_merge_request.title) expect(page).to have_content(merge_request.title).and have_no_content(closed_merge_request.title)
end end
...@@ -69,4 +81,35 @@ describe 'User views open merge requests' do ...@@ -69,4 +81,35 @@ describe 'User views open merge requests' do
end end
end end
end end
end
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))
end
include_examples 'shows merge requests'
end
end
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 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 */ /* 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'); preloadFixtures('static/gl_field_errors.html.raw');
describe('GL Style Field Errors', function() {
beforeEach(function() { beforeEach(function() {
loadFixtures('static/gl_field_errors.html.raw'); loadFixtures('static/gl_field_errors.html.raw');
const $form = this.$form = $('form.gl-show-field-errors'); const $form = this.$form = $('form.gl-show-field-errors');
this.fieldErrors = new global.GlFieldErrors($form); this.fieldErrors = new GlFieldErrors($form);
}); });
it('should select the correct input elements', function() { it('should select the correct input elements', function() {
...@@ -106,5 +105,4 @@ import '~/gl_field_errors'; ...@@ -106,5 +105,4 @@ import '~/gl_field_errors';
expect(noTitleErrorElem.text()).toBe('This field is required.'); expect(noTitleErrorElem.text()).toBe('This field is required.');
expect(hasTitleErrorElem.text()).toBe('Please provide a valid email address.'); expect(hasTitleErrorElem.text()).toBe('Please provide a valid email address.');
}); });
}); });
})(window.gl || (window.gl = {}));
import autosize from 'vendor/autosize'; import autosize from 'vendor/autosize';
import '~/gl_form'; import GLForm from '~/gl_form';
import '~/lib/utils/text_utility'; import '~/lib/utils/text_utility';
import '~/lib/utils/common_utils'; import '~/lib/utils/common_utils';
window.autosize = autosize; window.autosize = autosize;
describe('GLForm', () => { 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 () { describe('when instantiated', function () {
beforeEach((done) => { beforeEach((done) => {
this.form = $('<form class="gfm-form"><textarea class="js-gfm-input"></form>'); 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'; ...@@ -431,19 +431,17 @@ import '~/notes';
}); });
describe('putEditFormInPlace', () => { describe('putEditFormInPlace', () => {
it('should call gl.GLForm with GFM parameter passed through', () => { it('should call GLForm with GFM parameter passed through', () => {
spyOn(gl, 'GLForm'); const notes = new Notes('', []);
const $el = $(`
<div>
<form></form>
</div>
`);
const $el = jasmine.createSpyObj('$form', ['find', 'closest']); notes.putEditFormInPlace($el);
$el.find.and.returnValue($('<div>'));
$el.closest.and.returnValue($('<div>'));
Notes.prototype.putEditFormInPlace.call({ expect(notes.glForm.enableGFM).toBeTruthy();
getEditFormSelector: () => '',
enableGFM: true
}, $el);
expect(gl.GLForm).toHaveBeenCalledWith(jasmine.any(Object), true);
}); });
}); });
......
...@@ -109,12 +109,16 @@ describe('PrometheusMetrics', () => { ...@@ -109,12 +109,16 @@ describe('PrometheusMetrics', () => {
it('should show loader animation while response is being loaded and hide it when request is complete', (done) => { it('should show loader animation while response is being loaded and hide it when request is complete', (done) => {
const deferred = $.Deferred(); const deferred = $.Deferred();
spyOn($, 'getJSON').and.returnValue(deferred.promise()); spyOn($, 'ajax').and.returnValue(deferred.promise());
prometheusMetrics.loadActiveMetrics(); prometheusMetrics.loadActiveMetrics();
expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeFalsy(); 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 }); deferred.resolve({ data: metrics, success: true });
...@@ -126,7 +130,7 @@ describe('PrometheusMetrics', () => { ...@@ -126,7 +130,7 @@ describe('PrometheusMetrics', () => {
it('should show empty state if response failed to load', (done) => { it('should show empty state if response failed to load', (done) => {
const deferred = $.Deferred(); const deferred = $.Deferred();
spyOn($, 'getJSON').and.returnValue(deferred.promise()); spyOn($, 'ajax').and.returnValue(deferred.promise());
spyOn(prometheusMetrics, 'populateActiveMetrics'); spyOn(prometheusMetrics, 'populateActiveMetrics');
prometheusMetrics.loadActiveMetrics(); prometheusMetrics.loadActiveMetrics();
...@@ -142,7 +146,7 @@ describe('PrometheusMetrics', () => { ...@@ -142,7 +146,7 @@ describe('PrometheusMetrics', () => {
it('should populate metrics list once response is loaded', (done) => { it('should populate metrics list once response is loaded', (done) => {
const deferred = $.Deferred(); const deferred = $.Deferred();
spyOn($, 'getJSON').and.returnValue(deferred.promise()); spyOn($, 'ajax').and.returnValue(deferred.promise());
spyOn(prometheusMetrics, 'populateActiveMetrics'); spyOn(prometheusMetrics, 'populateActiveMetrics');
prometheusMetrics.loadActiveMetrics(); prometheusMetrics.loadActiveMetrics();
......
...@@ -2,11 +2,25 @@ import Vue from 'vue'; ...@@ -2,11 +2,25 @@ import Vue from 'vue';
import repoCommitSection from '~/repo/components/repo_commit_section.vue'; import repoCommitSection from '~/repo/components/repo_commit_section.vue';
import RepoStore from '~/repo/stores/repo_store'; import RepoStore from '~/repo/stores/repo_store';
import RepoService from '~/repo/services/repo_service'; import RepoService from '~/repo/services/repo_service';
import getSetTimeoutPromise from '../../helpers/set_timeout_promise_helper';
describe('RepoCommitSection', () => { describe('RepoCommitSection', () => {
const branch = 'master'; const branch = 'master';
const projectUrl = 'projectUrl'; const projectUrl = 'projectUrl';
const changedFiles = [{ let changedFiles;
let openedFiles;
RepoStore.projectUrl = projectUrl;
function createComponent(el) {
const RepoCommitSection = Vue.extend(repoCommitSection);
return new RepoCommitSection().$mount(el);
}
beforeEach(() => {
// Create a copy for each test because these can get modified directly
changedFiles = [{
id: 0, id: 0,
changed: true, changed: true,
url: `/namespace/${projectUrl}/blob/${branch}/dir/file0.ext`, url: `/namespace/${projectUrl}/blob/${branch}/dir/file0.ext`,
...@@ -19,20 +33,13 @@ describe('RepoCommitSection', () => { ...@@ -19,20 +33,13 @@ describe('RepoCommitSection', () => {
path: 'dir/file1.ext', path: 'dir/file1.ext',
newContent: 'b', newContent: 'b',
}]; }];
const openedFiles = changedFiles.concat([{ openedFiles = changedFiles.concat([{
id: 2, id: 2,
url: `/namespace/${projectUrl}/blob/${branch}/dir/file2.ext`, url: `/namespace/${projectUrl}/blob/${branch}/dir/file2.ext`,
path: 'dir/file2.ext', path: 'dir/file2.ext',
changed: false, changed: false,
}]); }]);
});
RepoStore.projectUrl = projectUrl;
function createComponent(el) {
const RepoCommitSection = Vue.extend(repoCommitSection);
return new RepoCommitSection().$mount(el);
}
it('renders a commit section', () => { it('renders a commit section', () => {
RepoStore.isCommitable = true; RepoStore.isCommitable = true;
...@@ -85,9 +92,13 @@ describe('RepoCommitSection', () => { ...@@ -85,9 +92,13 @@ describe('RepoCommitSection', () => {
expect(vm.$el.innerHTML).toBeFalsy(); 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 projectId = 'projectId';
const commitMessage = 'commitMessage'; const commitMessage = 'commitMessage';
beforeEach((done) => {
RepoStore.isCommitable = true; RepoStore.isCommitable = true;
RepoStore.currentBranch = branch; RepoStore.currentBranch = branch;
RepoStore.targetBranch = branch; RepoStore.targetBranch = branch;
...@@ -97,27 +108,55 @@ describe('RepoCommitSection', () => { ...@@ -97,27 +108,55 @@ describe('RepoCommitSection', () => {
// We need to append to body to get form `submit` events working // 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" // 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 // See https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#form-submission-algorithm
const el = document.createElement('div'); el = document.createElement('div');
document.body.appendChild(el); document.body.appendChild(el);
const vm = createComponent(el); vm = createComponent(el);
const commitMessageEl = vm.$el.querySelector('#commit-message');
const submitCommit = vm.$refs.submitCommit;
vm.commitMessage = commitMessage; 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(() => { Vue.nextTick(() => {
done();
});
});
afterEach(() => {
vm.$destroy();
el.remove();
});
it('shows commit message', () => {
const commitMessageEl = vm.$el.querySelector('#commit-message');
expect(commitMessageEl.value).toBe(commitMessage); expect(commitMessageEl.value).toBe(commitMessage);
expect(submitCommit.disabled).toBeFalsy(); });
spyOn(vm, 'makeCommit').and.callThrough(); it('allows you to submit', () => {
spyOn(RepoService, 'commitFiles').and.callFake(() => Promise.resolve()); 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(); submitCommit.click();
Vue.nextTick(() => { // Wait for the branch check to finish
expect(vm.makeCommit).toHaveBeenCalled(); getSetTimeoutPromise()
expect(submitCommit.querySelector('.fa-spinner.fa-spin')).toBeTruthy(); .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 args = RepoService.commitFiles.calls.allArgs()[0];
const { commit_message, actions, branch: payloadBranch } = args[0]; const { commit_message, actions, branch: payloadBranch } = args[0];
...@@ -131,9 +170,26 @@ describe('RepoCommitSection', () => { ...@@ -131,9 +170,26 @@ describe('RepoCommitSection', () => {
expect(actions[1].content).toEqual(openedFiles[1].newContent); expect(actions[1].content).toEqual(openedFiles[1].newContent);
expect(actions[0].file_path).toEqual(openedFiles[0].path); expect(actions[0].file_path).toEqual(openedFiles[0].path);
expect(actions[1].file_path).toEqual(openedFiles[1].path); expect(actions[1].file_path).toEqual(openedFiles[1].path);
})
done(); .then(done)
.catch(done.fail);
}); });
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', () => { ...@@ -143,6 +199,7 @@ describe('RepoCommitSection', () => {
const vm = { const vm = {
submitCommitsLoading: true, submitCommitsLoading: true,
changedFiles: new Array(10), changedFiles: new Array(10),
openedFiles: new Array(3),
commitMessage: 'commitMessage', commitMessage: 'commitMessage',
editMode: true, editMode: true,
}; };
......
...@@ -18,6 +18,10 @@ describe Gitlab::FileDetector do ...@@ -18,6 +18,10 @@ describe Gitlab::FileDetector do
expect(described_class.type_of('README.md')).to eq(:readme) expect(described_class.type_of('README.md')).to eq(:readme)
end 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 it 'returns the type of a changelog file' do
%w(CHANGELOG HISTORY CHANGES NEWS).each do |file| %w(CHANGELOG HISTORY CHANGES NEWS).each do |file|
expect(described_class.type_of(file)).to eq(:changelog) expect(described_class.type_of(file)).to eq(:changelog)
...@@ -52,6 +56,14 @@ describe Gitlab::FileDetector do ...@@ -52,6 +56,14 @@ describe Gitlab::FileDetector do
end end
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 it 'returns nil for an unknown file' do
expect(described_class.type_of('foo.txt')).to be_nil expect(described_class.type_of('foo.txt')).to be_nil
end end
......
...@@ -1510,6 +1510,21 @@ describe Gitlab::Git::Repository, seed_helper: true do ...@@ -1510,6 +1510,21 @@ describe Gitlab::Git::Repository, seed_helper: true do
end end
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) def create_remote_branch(repository, remote_name, branch_name, source_branch_name)
source_branch = repository.branches.find { |branch| branch.name == source_branch_name } source_branch = repository.branches.find { |branch| branch.name == source_branch_name }
rugged = repository.rugged rugged = repository.rugged
......
...@@ -1509,7 +1509,9 @@ describe Repository do ...@@ -1509,7 +1509,9 @@ describe Repository do
:gitignore, :gitignore,
:koding, :koding,
:gitlab_ci, :gitlab_ci,
:avatar :avatar,
:issue_template,
:merge_request_template
]) ])
repository.after_change_head repository.after_change_head
......
...@@ -227,13 +227,6 @@ describe API::Helpers do ...@@ -227,13 +227,6 @@ describe API::Helpers do
expect { current_user }.to raise_error /401/ expect { current_user }.to raise_error /401/
end 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 it "leaves user as is when sudo not specified" do
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
expect(current_user).to eq(user) expect(current_user).to eq(user)
...@@ -243,18 +236,25 @@ describe API::Helpers do ...@@ -243,18 +236,25 @@ describe API::Helpers do
expect(current_user).to eq(user) expect(current_user).to eq(user)
end 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 it 'does not allow revoked tokens' do
personal_access_token.revoke! personal_access_token.revoke!
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token 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 end
it 'does not allow expired tokens' do it 'does not allow expired tokens' do
personal_access_token.update_attributes!(expires_at: 1.day.ago) personal_access_token.update_attributes!(expires_at: 1.day.ago)
env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token 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
end end
......
...@@ -48,6 +48,21 @@ describe Auth::ContainerRegistryAuthenticationService do ...@@ -48,6 +48,21 @@ describe Auth::ContainerRegistryAuthenticationService do
end end
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 shared_examples 'an accessible' do
let(:access) do let(:access) do
[{ 'type' => 'repository', [{ 'type' => 'repository',
...@@ -56,7 +71,10 @@ describe Auth::ContainerRegistryAuthenticationService do ...@@ -56,7 +71,10 @@ describe Auth::ContainerRegistryAuthenticationService do
end end
it_behaves_like 'a valid token' 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 end
shared_examples 'an inaccessible' do shared_examples 'an inaccessible' do
...@@ -122,6 +140,17 @@ describe Auth::ContainerRegistryAuthenticationService do ...@@ -122,6 +140,17 @@ describe Auth::ContainerRegistryAuthenticationService do
context 'user authorization' do context 'user authorization' do
let(:current_user) { create(:user) } 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 context 'for private project' do
let(:project) { create(:project) } let(:project) { create(:project) }
...@@ -503,6 +532,16 @@ describe Auth::ContainerRegistryAuthenticationService do ...@@ -503,6 +532,16 @@ describe Auth::ContainerRegistryAuthenticationService do
end end
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 'unauthorized' do
context 'disallow to use scope-less authentication' do context 'disallow to use scope-less authentication' do
it_behaves_like 'a forbidden' it_behaves_like 'a forbidden'
...@@ -549,5 +588,14 @@ describe Auth::ContainerRegistryAuthenticationService do ...@@ -549,5 +588,14 @@ describe Auth::ContainerRegistryAuthenticationService do
it_behaves_like 'not a container repository factory' it_behaves_like 'not a container repository factory'
end end
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
end end
...@@ -201,7 +201,7 @@ describe MergeRequests::MergeService do ...@@ -201,7 +201,7 @@ describe MergeRequests::MergeService do
context 'source branch removal' do context 'source branch removal' do
context 'when the source branch is protected' do context 'when the source branch is protected' do
let(:service) 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 end
before do before do
...@@ -216,7 +216,7 @@ describe MergeRequests::MergeService do ...@@ -216,7 +216,7 @@ describe MergeRequests::MergeService do
context 'when the source branch is the default branch' do context 'when the source branch is the default branch' do
let(:service) 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 end
before do before do
...@@ -231,10 +231,10 @@ describe MergeRequests::MergeService do ...@@ -231,10 +231,10 @@ describe MergeRequests::MergeService do
context 'when the source branch can be removed' do context 'when the source branch can be removed' do
context 'when MR author set the source branch to be removed' do context 'when MR author set the source branch to be removed' do
let(:service) do let(:service) { described_class.new(project, user, commit_message: 'Awesome message') }
merge_request.merge_params['force_remove_source_branch'] = '1'
merge_request.save! before do
described_class.new(project, user, commit_message: 'Awesome message') merge_request.update_attribute(:merge_params, { 'force_remove_source_branch' => '1' })
end end
it 'removes the source branch using the author user' do it 'removes the source branch using the author user' do
...@@ -243,11 +243,20 @@ describe MergeRequests::MergeService do ...@@ -243,11 +243,20 @@ describe MergeRequests::MergeService do
.and_call_original .and_call_original
service.execute(merge_request) service.execute(merge_request)
end 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 end
context 'when MR merger set the source branch to be removed' do context 'when MR merger set the source branch to be removed' do
let(:service) 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 end
it 'removes the source branch using the current user' do it 'removes the source branch using the current user' do
......
...@@ -731,6 +731,18 @@ describe NotificationService, :mailer do ...@@ -731,6 +731,18 @@ describe NotificationService, :mailer do
should_not_email(@u_participating) should_not_email(@u_participating)
end 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 context 'confidential issues' do
let(:author) { create(:user) } let(:author) { create(:user) }
let(:assignee) { create(:user) } let(:assignee) { create(:user) }
......
...@@ -27,10 +27,10 @@ shared_examples_for 'allows the "read_user" scope' do ...@@ -27,10 +27,10 @@ shared_examples_for 'allows the "read_user" scope' do
stub_container_registry_config(enabled: true) stub_container_registry_config(enabled: true)
end end
it 'returns a "401" response' do it 'returns a "403" response' do
get api_call.call(path, user, personal_access_token: token) 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 end
end end
...@@ -74,10 +74,10 @@ shared_examples_for 'does not allow the "read_user" scope' do ...@@ -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 context 'when the requesting token has the "read_user" scope' do
let(:token) { create(:personal_access_token, scopes: ['read_user'], user: user) } 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) 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 end
end end
module EmailHelpers module EmailHelpers
def sent_to_user?(user, recipients = email_recipients) def sent_to_user(user, recipients: email_recipients)
recipients.include?(user.notification_email) recipients.count { |to| to == user.notification_email }
end end
def reset_delivered_emails! def reset_delivered_emails!
...@@ -10,17 +10,17 @@ module EmailHelpers ...@@ -10,17 +10,17 @@ module EmailHelpers
def should_only_email(*users, kind: :to) def should_only_email(*users, kind: :to)
recipients = email_recipients(kind: kind) 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) expect(recipients.count).to eq(users.count)
end end
def should_email(user, recipients = email_recipients) def should_email(user, times: 1, recipients: email_recipients)
expect(sent_to_user?(user, recipients)).to be_truthy expect(sent_to_user(user, recipients: recipients)).to eq(times)
end end
def should_not_email(user, recipients = email_recipients) def should_not_email(user, recipients: email_recipients)
expect(sent_to_user?(user, recipients)).to be_falsey should_email(user, times: 0, recipients: recipients)
end end
def should_not_email_anyone def should_not_email_anyone
......
...@@ -41,7 +41,8 @@ captures/ ...@@ -41,7 +41,8 @@ captures/
.idea/libraries .idea/libraries
# Keystore files # 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 # External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild .externalNativeBuild
......
...@@ -31,3 +31,12 @@ Makefile.in ...@@ -31,3 +31,12 @@ Makefile.in
# http://www.gnu.org/software/texinfo # http://www.gnu.org/software/texinfo
/texinfo.tex /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 /_build
/cover /cover
/deps /deps
/doc
/.fetch
erl_crash.dump erl_crash.dump
*.ez *.ez
*.beam *.beam
...@@ -10,3 +10,5 @@ ext/ ...@@ -10,3 +10,5 @@ ext/
modern.json modern.json
modern.jsonp modern.jsonp
resources/sass/.sass-cache/ resources/sass/.sass-cache/
resources/.arch-internal-preview.css
.arch-internal-preview.css
...@@ -19,4 +19,4 @@ slprj/ ...@@ -19,4 +19,4 @@ slprj/
octave-workspace octave-workspace
# Simulink autosave extension # Simulink autosave extension
.autosave *.autosave
...@@ -2,11 +2,17 @@ ...@@ -2,11 +2,17 @@
# #
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore # 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/ build/
DerivedData/ DerivedData/
*.moved-aside
## Various settings
*.pbxuser *.pbxuser
!default.pbxuser !default.pbxuser
*.mode1v3 *.mode1v3
...@@ -15,9 +21,3 @@ DerivedData/ ...@@ -15,9 +21,3 @@ DerivedData/
!default.mode2v3 !default.mode2v3
*.perspectivev3 *.perspectivev3
!default.perspectivev3 !default.perspectivev3
xcuserdata/
## Other
*.moved-aside
*.xccheckout
*.xcscmblueprint
# General # General
*.DS_Store .DS_Store
.AppleDouble .AppleDouble
.LSOverride .LSOverride
......
...@@ -251,7 +251,7 @@ ...@@ -251,7 +251,7 @@
/administrator/language/en-GB/en-GB.tpl_hathor.sys.ini /administrator/language/en-GB/en-GB.tpl_hathor.sys.ini
/administrator/language/en-GB/en-GB.xml /administrator/language/en-GB/en-GB.xml
/administrator/language/overrides/* /administrator/language/overrides/*
/administrator/logs/index.html /administrator/logs/*
/administrator/manifests/* /administrator/manifests/*
/administrator/modules/mod_custom/* /administrator/modules/mod_custom/*
/administrator/modules/mod_feed/* /administrator/modules/mod_feed/*
......
...@@ -18,3 +18,6 @@ _build/ ...@@ -18,3 +18,6 @@ _build/
# oasis generated files # oasis generated files
setup.data setup.data
setup.log setup.log
# Merlin configuring file for Vim and Emacs
.merlin
...@@ -23,6 +23,7 @@ wheels/ ...@@ -23,6 +23,7 @@ wheels/
*.egg-info/ *.egg-info/
.installed.cfg .installed.cfg
*.egg *.egg
MANIFEST
# PyInstaller # PyInstaller
# Usually these files are written by a python script from a template # Usually these files are written by a python script from a template
...@@ -51,6 +52,8 @@ coverage.xml ...@@ -51,6 +52,8 @@ coverage.xml
# Django stuff: # Django stuff:
*.log *.log
.static_storage/
.media/
local_settings.py local_settings.py
# Flask stuff: # Flask stuff:
...@@ -84,6 +87,8 @@ celerybeat-schedule ...@@ -84,6 +87,8 @@ celerybeat-schedule
env/ env/
venv/ venv/
ENV/ ENV/
env.bak/
venv.bak/
# Spyder project settings # Spyder project settings
.spyderproject .spyderproject
......
...@@ -31,11 +31,9 @@ ui_*.h ...@@ -31,11 +31,9 @@ ui_*.h
Makefile* Makefile*
*build-* *build-*
# Qt unit tests # Qt unit tests
target_wrapper.* target_wrapper.*
# QtCreator # QtCreator
*.autosave *.autosave
......
...@@ -13,6 +13,7 @@ ...@@ -13,6 +13,7 @@
## Intermediate documents: ## Intermediate documents:
*.dvi *.dvi
*.xdv
*-converted-to.* *-converted-to.*
# these rules might exclude image files for figures etc. # these rules might exclude image files for figures etc.
# *.ps # *.ps
......
...@@ -5,3 +5,6 @@ ...@@ -5,3 +5,6 @@
# Module directory # Module directory
.terraform/ .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 # Note: VisualStudio gitignore rules may also be relevant
# Umbraco # Umbraco
......
...@@ -96,6 +96,9 @@ ipch/ ...@@ -96,6 +96,9 @@ ipch/
*.vspx *.vspx
*.sap *.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace # TFS 2012 Local Workspace
$tf/ $tf/
...@@ -297,3 +300,6 @@ __pycache__/ ...@@ -297,3 +300,6 @@ __pycache__/
*.btm.cs *.btm.cs
*.odx.cs *.odx.cs
*.xsd.cs *.xsd.cs
# OpenCover UI analysis results
OpenCover/
...@@ -19,7 +19,6 @@ temp/ ...@@ -19,7 +19,6 @@ temp/
data/DoctrineORMModule/Proxy/ data/DoctrineORMModule/Proxy/
data/DoctrineORMModule/cache/ data/DoctrineORMModule/cache/
# Legacy ZF1 # Legacy ZF1
demos/ demos/
extras/documentation extras/documentation
...@@ -29,7 +29,7 @@ format: ...@@ -29,7 +29,7 @@ format:
compile: compile:
stage: build stage: build
script: script:
- go build -race -ldflags "-extldflags '-static'" -o mybinary - go build -race -ldflags "-extldflags '-static'" -o $CI_PROJECT_DIR/mybinary
artifacts: artifacts:
paths: paths:
- mybinary - mybinary
...@@ -7,8 +7,8 @@ ...@@ -7,8 +7,8 @@
# This template will build and test your projects as well as create the documentation. # This template will build and test your projects as well as create the documentation.
# #
# * Caches downloaded dependencies and plugins between invocation. # * Caches downloaded dependencies and plugins between invocation.
# * Does only verify merge requests but deploy built artifacts of the # * Verify but don't deploy merge requests.
# master branch. # * Deploy built artifacts from master branch only.
# * Shows how to use multiple jobs in test stage for verifying functionality # * Shows how to use multiple jobs in test stage for verifying functionality
# with multiple JDKs. # with multiple JDKs.
# * Uses site:stage to collect the documentation for multi-module projects. # * Uses site:stage to collect the documentation for multi-module projects.
...@@ -20,7 +20,7 @@ variables: ...@@ -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" 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 # 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. # 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" MAVEN_CLI_OPTS: "--batch-mode --errors --fail-at-end --show-version -DinstallAtEnd=true -DdeployAtEnd=true"
# Cache downloaded dependencies and plugins between builds. # Cache downloaded dependencies and plugins between builds.
...@@ -100,4 +100,3 @@ pages: ...@@ -100,4 +100,3 @@ pages:
- public - public
only: only:
- master - 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