Commit 2a8e066a authored by Valery Sizov's avatar Valery Sizov

Merge branch 'ce_upstream' into 'master'

CE upstream

See merge request !991
parents 03624742 08e187b9
...@@ -33,6 +33,7 @@ gem 'omniauth-shibboleth', '~> 1.2.0' ...@@ -33,6 +33,7 @@ gem 'omniauth-shibboleth', '~> 1.2.0'
gem 'omniauth-twitter', '~> 1.2.0' gem 'omniauth-twitter', '~> 1.2.0'
gem 'omniauth_crowd', '~> 2.2.0' gem 'omniauth_crowd', '~> 2.2.0'
gem 'gssapi', group: :kerberos gem 'gssapi', group: :kerberos
gem 'omniauth-authentiq', '~> 0.2.0'
gem 'rack-oauth2', '~> 1.2.1' gem 'rack-oauth2', '~> 1.2.1'
gem 'jwt' gem 'jwt'
......
...@@ -452,6 +452,8 @@ GEM ...@@ -452,6 +452,8 @@ GEM
rack (>= 1.0, < 3) rack (>= 1.0, < 3)
omniauth-auth0 (1.4.1) omniauth-auth0 (1.4.1)
omniauth-oauth2 (~> 1.1) omniauth-oauth2 (~> 1.1)
omniauth-authentiq (0.2.2)
omniauth-oauth2 (~> 1.3, >= 1.3.1)
omniauth-azure-oauth2 (0.0.6) omniauth-azure-oauth2 (0.0.6)
jwt (~> 1.0) jwt (~> 1.0)
omniauth (~> 1.0) omniauth (~> 1.0)
...@@ -930,6 +932,7 @@ DEPENDENCIES ...@@ -930,6 +932,7 @@ DEPENDENCIES
oj (~> 2.17.4) oj (~> 2.17.4)
omniauth (~> 1.3.1) omniauth (~> 1.3.1)
omniauth-auth0 (~> 1.4.1) omniauth-auth0 (~> 1.4.1)
omniauth-authentiq (~> 0.2.0)
omniauth-azure-oauth2 (~> 0.0.6) omniauth-azure-oauth2 (~> 0.0.6)
omniauth-cas3 (~> 1.1.2) omniauth-cas3 (~> 1.1.2)
omniauth-facebook (~> 4.0.0) omniauth-facebook (~> 4.0.0)
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
(function() { (function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
var AUTO_SCROLL_OFFSET = 75;
this.Build = (function() { this.Build = (function() {
Build.interval = null; Build.interval = null;
...@@ -19,6 +20,17 @@ ...@@ -19,6 +20,17 @@
this.buildStage = options.buildStage; this.buildStage = options.buildStage;
this.updateDropdown = bind(this.updateDropdown, this); this.updateDropdown = bind(this.updateDropdown, this);
this.$document = $(document); this.$document = $(document);
this.$body = $('body');
this.$buildTrace = $('#build-trace');
this.$autoScrollContainer = $('.autoscroll-container');
this.$autoScrollStatus = $('#autoscroll-status');
this.$autoScrollStatusText = this.$autoScrollStatus.find('.status-text');
this.$upBuildTrace = $('#up-build-trace');
this.$downBuildTrace = $('#down-build-trace');
this.$scrollTopBtn = $('#scroll-top');
this.$scrollBottomBtn = $('#scroll-bottom');
this.$buildRefreshAnimation = $('.js-build-refresh');
clearInterval(Build.interval); clearInterval(Build.interval);
// Init breakpoint checker // Init breakpoint checker
this.bp = Breakpoints.get(); this.bp = Breakpoints.get();
...@@ -32,6 +44,7 @@ ...@@ -32,6 +44,7 @@
this.$document.off('click', '.js-sidebar-build-toggle').on('click', '.js-sidebar-build-toggle', this.sidebarOnClick.bind(this)); this.$document.off('click', '.js-sidebar-build-toggle').on('click', '.js-sidebar-build-toggle', this.sidebarOnClick.bind(this));
this.$document.off('click', '.stage-item').on('click', '.stage-item', this.updateDropdown); this.$document.off('click', '.stage-item').on('click', '.stage-item', this.updateDropdown);
this.$document.on('scroll', this.initScrollMonitor.bind(this));
$(window).off('resize.build').on('resize.build', this.sidebarOnResize.bind(this)); $(window).off('resize.build').on('resize.build', this.sidebarOnResize.bind(this));
$('a', this.$buildScroll).off('click.stepTrace').on('click.stepTrace', this.stepTrace); $('a', this.$buildScroll).off('click.stepTrace').on('click.stepTrace', this.stepTrace);
this.updateArtifactRemoveDate(); this.updateArtifactRemoveDate();
...@@ -40,18 +53,6 @@ ...@@ -40,18 +53,6 @@
this.initScrollButtonAffix(); this.initScrollButtonAffix();
} }
if (this.buildStatus === "running" || this.buildStatus === "pending") { if (this.buildStatus === "running" || this.buildStatus === "pending") {
// Bind autoscroll button to follow build output
$('#autoscroll-button').on('click', function() {
var state;
state = $(this).data("state");
if ("enabled" === state) {
$(this).data("state", "disabled");
return $(this).text("Enable autoscroll");
} else {
$(this).data("state", "enabled");
return $(this).text("Disable autoscroll");
}
});
Build.interval = setInterval((function(_this) { Build.interval = setInterval((function(_this) {
// Check for new build output if user still watching build page // Check for new build output if user still watching build page
// Only valid for runnig build when output changes during time // Only valid for runnig build when output changes during time
...@@ -91,9 +92,10 @@ ...@@ -91,9 +92,10 @@
success: function(buildData) { success: function(buildData) {
$('.js-build-output').html(buildData.trace_html); $('.js-build-output').html(buildData.trace_html);
if (removeRefreshStatuses.indexOf(buildData.status) >= 0) { if (removeRefreshStatuses.indexOf(buildData.status) >= 0) {
return $('.js-build-refresh').remove(); this.initScrollMonitor();
} return this.$buildRefreshAnimation.remove();
} }
}.bind(this)
}); });
}; };
...@@ -122,22 +124,95 @@ ...@@ -122,22 +124,95 @@
}; };
Build.prototype.checkAutoscroll = function() { Build.prototype.checkAutoscroll = function() {
if ("enabled" === $("#autoscroll-button").data("state")) { if (this.$autoScrollStatus.data("state") === "enabled") {
return $("html,body").scrollTop($("#build-trace").height()); return $("html,body").scrollTop(this.$buildTrace.height());
}
// Handle a situation where user started new build
// but never scrolled a page
if (!this.$scrollTopBtn.is(':visible') &&
!this.$scrollBottomBtn.is(':visible') &&
!gl.utils.isInViewport(this.$downBuildTrace.get(0))) {
this.$scrollBottomBtn.show();
} }
}; };
Build.prototype.initScrollButtonAffix = function() { Build.prototype.initScrollButtonAffix = function() {
var $body, $buildTrace; // Hide everything initially
$body = $('body'); this.$scrollTopBtn.hide();
$buildTrace = $('#build-trace'); this.$scrollBottomBtn.hide();
return this.$buildScroll.affix({ this.$autoScrollContainer.hide();
offset: {
bottom: function() {
return $body.outerHeight() - ($buildTrace.outerHeight() + $buildTrace.offset().top);
} }
// Page scroll listener to detect if user has scrolling page
// and handle following cases
// 1) User is at Top of Build Log;
// - Hide Top Arrow button
// - Show Bottom Arrow button
// - Disable Autoscroll and hide indicator (when build is running)
// 2) User is at Bottom of Build Log;
// - Show Top Arrow button
// - Hide Bottom Arrow button
// - Enable Autoscroll and show indicator (when build is running)
// 3) User is somewhere in middle of Build Log;
// - Show Top Arrow button
// - Show Bottom Arrow button
// - Disable Autoscroll and hide indicator (when build is running)
Build.prototype.initScrollMonitor = function() {
if (!gl.utils.isInViewport(this.$upBuildTrace.get(0)) && !gl.utils.isInViewport(this.$downBuildTrace.get(0))) {
// User is somewhere in middle of Build Log
this.$scrollTopBtn.show();
if (this.buildStatus === 'success' || this.buildStatus === 'failed') { // Check if Build is completed
this.$scrollBottomBtn.show();
} else if (this.$buildRefreshAnimation.is(':visible') && !gl.utils.isInViewport(this.$buildRefreshAnimation.get(0))) {
this.$scrollBottomBtn.show();
} else {
this.$scrollBottomBtn.hide();
}
// Hide Autoscroll Status Indicator
if (this.$scrollBottomBtn.is(':visible')) {
this.$autoScrollContainer.hide();
this.$autoScrollStatusText.removeClass('animate');
} else {
this.$autoScrollContainer.css({ top: this.$body.outerHeight() - AUTO_SCROLL_OFFSET }).show();
this.$autoScrollStatusText.addClass('animate');
}
} else if (gl.utils.isInViewport(this.$upBuildTrace.get(0)) && !gl.utils.isInViewport(this.$downBuildTrace.get(0))) {
// User is at Top of Build Log
this.$scrollTopBtn.hide();
this.$scrollBottomBtn.show();
this.$autoScrollContainer.hide();
this.$autoScrollStatusText.removeClass('animate');
} else if ((!gl.utils.isInViewport(this.$upBuildTrace.get(0)) && gl.utils.isInViewport(this.$downBuildTrace.get(0))) ||
(this.$buildRefreshAnimation.is(':visible') && gl.utils.isInViewport(this.$buildRefreshAnimation.get(0)))) {
// User is at Bottom of Build Log
this.$scrollTopBtn.show();
this.$scrollBottomBtn.hide();
// Show and Reposition Autoscroll Status Indicator
this.$autoScrollContainer.css({ top: this.$body.outerHeight() - AUTO_SCROLL_OFFSET }).show();
this.$autoScrollStatusText.addClass('animate');
} else if (gl.utils.isInViewport(this.$upBuildTrace.get(0)) && gl.utils.isInViewport(this.$downBuildTrace.get(0))) {
// Build Log height is small
this.$scrollTopBtn.hide();
this.$scrollBottomBtn.hide();
// Hide Autoscroll Status Indicator
this.$autoScrollContainer.hide();
this.$autoScrollStatusText.removeClass('animate');
}
if (this.buildStatus === "running" || this.buildStatus === "pending") {
// Check if Refresh Animation is in Viewport and enable Autoscroll, disable otherwise.
this.$autoScrollStatus.data("state", gl.utils.isInViewport(this.$buildRefreshAnimation.get(0)) ? 'enabled' : 'disabled');
} }
});
}; };
Build.prototype.shouldHideSidebarForViewport = function() { Build.prototype.shouldHideSidebarForViewport = function() {
......
...@@ -141,7 +141,6 @@ ...@@ -141,7 +141,6 @@
new MergedButtons(); new MergedButtons();
break; break;
case 'projects:merge_requests:commits': case 'projects:merge_requests:commits':
case 'projects:merge_requests:builds':
new MergedButtons(); new MergedButtons();
break; break;
case 'projects:merge_requests:pipelines': case 'projects:merge_requests:pipelines':
...@@ -171,9 +170,6 @@ ...@@ -171,9 +170,6 @@
container: '.js-pipeline-table', container: '.js-pipeline-table',
}); });
break; break;
case 'projects:commit:builds':
new gl.Pipelines();
break;
case 'projects:commits:show': case 'projects:commits:show':
case 'projects:activity': case 'projects:activity':
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
......
...@@ -112,7 +112,6 @@ ...@@ -112,7 +112,6 @@
return value.path != null ? this.Emoji.template : this.Loading.template; return value.path != null ? this.Emoji.template : this.Loading.template;
}.bind(this), }.bind(this),
insertTpl: ':${name}:', insertTpl: ':${name}:',
startWithSpace: false,
skipSpecialCharacterTest: true, skipSpecialCharacterTest: true,
data: this.defaultLoadingData, data: this.defaultLoadingData,
callbacks: { callbacks: {
...@@ -129,7 +128,6 @@ ...@@ -129,7 +128,6 @@
}.bind(this), }.bind(this),
insertTpl: '${atwho-at}${username}', insertTpl: '${atwho-at}${username}',
searchKey: 'search', searchKey: 'search',
startWithSpace: false,
alwaysHighlightFirst: true, alwaysHighlightFirst: true,
skipSpecialCharacterTest: true, skipSpecialCharacterTest: true,
data: this.defaultLoadingData, data: this.defaultLoadingData,
...@@ -172,7 +170,6 @@ ...@@ -172,7 +170,6 @@
}.bind(this), }.bind(this),
data: this.defaultLoadingData, data: this.defaultLoadingData,
insertTpl: '${atwho-at}${id}', insertTpl: '${atwho-at}${id}',
startWithSpace: false,
callbacks: { callbacks: {
sorter: this.DefaultOptions.sorter, sorter: this.DefaultOptions.sorter,
filter: this.DefaultOptions.filter, filter: this.DefaultOptions.filter,
...@@ -200,7 +197,6 @@ ...@@ -200,7 +197,6 @@
displayTpl: function(value) { displayTpl: function(value) {
return value.title != null ? this.Milestones.template : this.Loading.template; return value.title != null ? this.Milestones.template : this.Loading.template;
}.bind(this), }.bind(this),
startWithSpace: false,
data: this.defaultLoadingData, data: this.defaultLoadingData,
callbacks: { callbacks: {
matcher: this.DefaultOptions.matcher, matcher: this.DefaultOptions.matcher,
...@@ -225,7 +221,6 @@ ...@@ -225,7 +221,6 @@
at: '!', at: '!',
alias: 'mergerequests', alias: 'mergerequests',
searchKey: 'search', searchKey: 'search',
startWithSpace: false,
displayTpl: function(value) { displayTpl: function(value) {
return value.title != null ? this.Issues.template : this.Loading.template; return value.title != null ? this.Issues.template : this.Loading.template;
}.bind(this), }.bind(this),
...@@ -259,7 +254,6 @@ ...@@ -259,7 +254,6 @@
return this.isLoading(value) ? this.Loading.template : this.Labels.template; return this.isLoading(value) ? this.Loading.template : this.Labels.template;
}.bind(this), }.bind(this),
insertTpl: '${atwho-at}${title}', insertTpl: '${atwho-at}${title}',
startWithSpace: false,
callbacks: { callbacks: {
matcher: this.DefaultOptions.matcher, matcher: this.DefaultOptions.matcher,
sorter: this.DefaultOptions.sorter, sorter: this.DefaultOptions.sorter,
...@@ -379,14 +373,7 @@ ...@@ -379,14 +373,7 @@
togglePreventSelection(isPrevented = !!this.setting.tabSelectsMatch) { togglePreventSelection(isPrevented = !!this.setting.tabSelectsMatch) {
this.setting.tabSelectsMatch = !isPrevented; this.setting.tabSelectsMatch = !isPrevented;
this.setting.spaceSelectsMatch = !isPrevented; this.setting.spaceSelectsMatch = !isPrevented;
const eventListenerAction = `${isPrevented ? 'add' : 'remove'}EventListener`;
this.$inputor[0][eventListenerAction]('keydown', gl.GfmAutoComplete.preventSpaceTabEnter);
}, },
preventSpaceTabEnter(e) {
const key = e.which || e.keyCode;
const preventables = [9, 13, 32];
if (preventables.indexOf(key) > -1) e.preventDefault();
}
}; };
}).call(this); }).call(this);
/* eslint-disable func-names, no-var, camelcase, no-unused-vars, object-shorthand, space-before-function-paren, no-return-assign, comma-dangle, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, prefer-arrow-callback, prefer-const, padded-blocks, wrap-iife, max-len */ /* eslint-disable no-param-reassign, func-names, no-var, camelcase, no-unused-vars, object-shorthand, space-before-function-paren, no-return-assign, comma-dangle, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, prefer-arrow-callback, prefer-const, padded-blocks, wrap-iife, max-len */
/* global Issuable */ /* global Issuable */
/* global Turbolinks */ /* global Turbolinks */
(function() { ((global) => {
var issuable_created; var issuable_created;
issuable_created = false; issuable_created = false;
this.Issuable = { global.Issuable = {
init: function() { init: function() {
Issuable.initTemplates(); Issuable.initTemplates();
Issuable.initSearch(); Issuable.initSearch();
...@@ -111,7 +111,11 @@ ...@@ -111,7 +111,11 @@
filterResults: (function(_this) { filterResults: (function(_this) {
return function(form) { return function(form) {
var formAction, formData, issuesUrl; var formAction, formData, issuesUrl;
formData = form.serialize(); formData = form.serializeArray();
formData = formData.filter(function(data) {
return data.value !== '';
});
formData = $.param(formData);
formAction = form.attr('action'); formAction = form.attr('action');
issuesUrl = formAction; issuesUrl = formAction;
issuesUrl += "" + (formAction.indexOf('?') < 0 ? '?' : '&'); issuesUrl += "" + (formAction.indexOf('?') < 0 ? '?' : '&');
...@@ -184,4 +188,4 @@ ...@@ -184,4 +188,4 @@
} }
}; };
}).call(this); })(window);
...@@ -93,6 +93,19 @@ ...@@ -93,6 +93,19 @@
} }
}; };
// Check if element scrolled into viewport from above or below
// Courtesy http://stackoverflow.com/a/7557433/414749
w.gl.utils.isInViewport = function(el) {
var rect = el.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= window.innerHeight &&
rect.right <= window.innerWidth
);
};
gl.utils.getPagePath = function() { gl.utils.getPagePath = function() {
return $('body').data('page').split(':')[0]; return $('body').data('page').split(':')[0];
}; };
......
...@@ -59,16 +59,13 @@ ...@@ -59,16 +59,13 @@
class MergeRequestTabs { class MergeRequestTabs {
constructor({ action, setUrl, buildsLoaded, stubLocation } = {}) { constructor({ action, setUrl, stubLocation } = {}) {
this.diffsLoaded = false; this.diffsLoaded = false;
this.buildsLoaded = false;
this.pipelinesLoaded = false; this.pipelinesLoaded = false;
this.commitsLoaded = false; this.commitsLoaded = false;
this.fixedLayoutPref = null; this.fixedLayoutPref = null;
this.setUrl = setUrl !== undefined ? setUrl : true; this.setUrl = setUrl !== undefined ? setUrl : true;
this.buildsLoaded = buildsLoaded || false;
this.setCurrentAction = this.setCurrentAction.bind(this); this.setCurrentAction = this.setCurrentAction.bind(this);
this.tabShown = this.tabShown.bind(this); this.tabShown = this.tabShown.bind(this);
this.showTab = this.showTab.bind(this); this.showTab = this.showTab.bind(this);
...@@ -119,10 +116,6 @@ ...@@ -119,10 +116,6 @@
$.scrollTo('.merge-request-details .merge-request-tabs', { $.scrollTo('.merge-request-details .merge-request-tabs', {
offset: -navBarHeight, offset: -navBarHeight,
}); });
} else if (action === 'builds') {
this.loadBuilds($target.attr('href'));
this.expandView();
this.resetViewContainer();
} else if (action === 'pipelines') { } else if (action === 'pipelines') {
this.loadPipelines($target.attr('href')); this.loadPipelines($target.attr('href'));
this.expandView(); this.expandView();
...@@ -180,8 +173,8 @@ ...@@ -180,8 +173,8 @@
setCurrentAction(action) { setCurrentAction(action) {
this.currentAction = action === 'show' ? 'notes' : action; this.currentAction = action === 'show' ? 'notes' : action;
// Remove a trailing '/commits' '/diffs' '/builds' '/pipelines' '/new' '/new/diffs' // Remove a trailing '/commits' '/diffs' '/pipelines' '/new' '/new/diffs'
let newState = location.pathname.replace(/\/(commits|diffs|builds|pipelines|new|new\/diffs)(\.html)?\/?$/, ''); let newState = location.pathname.replace(/\/(commits|diffs|pipelines|new|new\/diffs)(\.html)?\/?$/, '');
// Append the new action if we're on a tab other than 'notes' // Append the new action if we're on a tab other than 'notes'
if (this.currentAction !== 'notes') { if (this.currentAction !== 'notes') {
...@@ -255,22 +248,6 @@ ...@@ -255,22 +248,6 @@
}); });
} }
loadBuilds(source) {
if (this.buildsLoaded) {
return;
}
this.ajaxGet({
url: `${source}.json`,
success: (data) => {
document.querySelector('div#builds').innerHTML = data.html;
gl.utils.localTimeAgo($('.js-timeago', 'div#builds'));
this.buildsLoaded = true;
new gl.Pipelines();
this.scrollToElement('#builds');
},
});
}
loadPipelines(source) { loadPipelines(source) {
if (this.pipelinesLoaded) { if (this.pipelinesLoaded) {
return; return;
......
...@@ -74,7 +74,7 @@ ...@@ -74,7 +74,7 @@
MergeRequestWidget.prototype.addEventListeners = function() { MergeRequestWidget.prototype.addEventListeners = function() {
var allowedPages; var allowedPages;
allowedPages = ['show', 'commits', 'builds', 'pipelines', 'changes']; allowedPages = ['show', 'commits', 'pipelines', 'changes'];
$(document).on('page:change.merge_request', (function(_this) { $(document).on('page:change.merge_request', (function(_this) {
return function() { return function() {
var page; var page;
...@@ -190,7 +190,6 @@ ...@@ -190,7 +190,6 @@
message = message.replace('{{title}}', data.title); message = message.replace('{{title}}', data.title);
notify(title, message, _this.opts.gitlab_icon, function() { notify(title, message, _this.opts.gitlab_icon, function() {
this.close(); this.close();
return Turbolinks.visit(_this.opts.builds_path);
}); });
} }
} }
......
/* global merge_request_widget */
(() => {
$(() => {
/* TODO: This needs a better home, or should be refactored. It was previously contained
* in a script tag in app/views/projects/merge_requests/widget/open/_accept.html.haml,
* but Vue chokes on script tags and prevents their execution. So it was moved here
* temporarily.
* */
if ($('.accept-mr-form').length) {
$('.accept-mr-form').on('ajax:send', () => {
$('.accept-mr-form :input').disable();
});
$('.accept_merge_request').on('click', () => {
$('.js-merge-button').html('<i class="fa fa-spinner fa-spin"></i> Merge in progress');
});
$('.merge_when_build_succeeds').on('click', () => {
$('#merge_when_build_succeeds').val('1');
});
$('.js-merge-dropdown a').on('click', (e) => {
e.preventDefault();
$(this).closest('form').submit();
});
} else if ($('.rebase-in-progress').length) {
merge_request_widget.rebaseInProgress();
} else if ($('.rebase-mr-form').length) {
$('.rebase-mr-form').on('ajax:send', () => {
$('.rebase-mr-form :input').disable();
});
$('.js-rebase-button').on('click', () => {
$('.js-rebase-button').html("<i class='fa fa-spinner fa-spin'></i> Rebase in progress");
});
} else {
merge_request_widget.getMergeStatus();
}
});
})();
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
@import "framework/asciidoctor.scss"; @import "framework/asciidoctor.scss";
@import "framework/blocks.scss"; @import "framework/blocks.scss";
@import "framework/buttons.scss"; @import "framework/buttons.scss";
@import "framework/badges.scss";
@import "framework/calendar.scss"; @import "framework/calendar.scss";
@import "framework/callout.scss"; @import "framework/callout.scss";
@import "framework/common.scss"; @import "framework/common.scss";
......
.badge {
font-weight: normal;
background-color: $badge-bg;
color: $badge-color;
vertical-align: baseline;
}
.badge-dark {
background-color: $badge-bg-dark;
color: $badge-color-dark;
}
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
padding: 20px; padding: 20px;
color: $gl-gray; color: $gl-gray;
font-weight: normal; font-weight: normal;
font-size: 16px; font-size: 14px;
line-height: 36px; line-height: 36px;
&.diff-collapsed { &.diff-collapsed {
......
...@@ -76,13 +76,6 @@ ...@@ -76,13 +76,6 @@
color: $black; color: $black;
} }
} }
.badge {
font-weight: normal;
background-color: $nav-badge-bg;
color: $gl-gray-light;
vertical-align: baseline;
}
} }
&.sub-nav { &.sub-nav {
......
...@@ -33,7 +33,7 @@ ...@@ -33,7 +33,7 @@
@import "bootstrap/labels"; @import "bootstrap/labels";
@import "bootstrap/badges"; @import "bootstrap/badges";
@import "bootstrap/alerts"; @import "bootstrap/alerts";
// @import "bootstrap/progress-bars"; @import "bootstrap/progress-bars";
@import "bootstrap/list-group"; @import "bootstrap/list-group";
@import "bootstrap/wells"; @import "bootstrap/wells";
@import "bootstrap/close"; @import "bootstrap/close";
......
...@@ -136,8 +136,8 @@ $md-area-border: #ddd; ...@@ -136,8 +136,8 @@ $md-area-border: #ddd;
/* /*
* Code * Code
*/ */
$code_font_size: 13px; $code_font_size: 12px;
$code_line_height: 1.5; $code_line_height: 1.6;
/* /*
* Padding * Padding
...@@ -286,6 +286,14 @@ $btn-active-gray: #ececec; ...@@ -286,6 +286,14 @@ $btn-active-gray: #ececec;
$btn-active-gray-light: e4e7ed; $btn-active-gray-light: e4e7ed;
$btn-white-active: #848484; $btn-white-active: #848484;
/*
* Badges
*/
$badge-bg: #f3f3f3;
$badge-bg-dark: #eee;
$badge-color: #929292;
$badge-color-dark: #8f8f8f;
/* /*
* Award emoji * Award emoji
*/ */
......
@keyframes fade-out-status {
0%, 50% { opacity: 1; }
100% { opacity: 0; }
}
@keyframes blinking-dots {
0% {
background-color: rgba($white-light, 1);
box-shadow: 12px 0 0 0 rgba($white-light,0.2),
24px 0 0 0 rgba($white-light,0.2);
}
25% {
background-color: rgba($white-light, 0.4);
box-shadow: 12px 0 0 0 rgba($white-light,2),
24px 0 0 0 rgba($white-light,0.2);
}
75% {
background-color: rgba($white-light, 0.4);
box-shadow: 12px 0 0 0 rgba($white-light,0.2),
24px 0 0 0 rgba($white-light,1);
}
100% {
background-color: rgba($white-light, 1);
box-shadow: 12px 0 0 0 rgba($white-light,0.2),
24px 0 0 0 rgba($white-light,0.2);
}
}
.build-page { .build-page {
pre.trace { pre.trace {
background: $builds-trace-bg; background: $builds-trace-bg;
...@@ -14,47 +45,101 @@ ...@@ -14,47 +45,101 @@
} }
} }
.scroll-controls { .environment-information {
background-color: $gray-light;
border: 1px solid $border-color;
padding: 12px $gl-padding;
border-radius: $border-radius-default;
svg {
position: relative;
top: 1px;
margin-right: 5px;
}
}
}
.scroll-controls {
height: 100%;
.scroll-step { .scroll-step {
width: 31px; width: 31px;
margin: 0 0 0 auto; margin: 0 0 0 auto;
} }
&.affix-bottom { .scroll-link,
position: absolute; .autoscroll-container {
right: 25px; right: 25px;
z-index: 1;
} }
&.affix { .scroll-link {
right: 25px; position: fixed;
bottom: 15px; display: block;
z-index: 1; margin-bottom: 10px;
&.scroll-top .gitlab-icon-scroll-up-hover,
&.scroll-top:hover .gitlab-icon-scroll-up,
&.scroll-bottom .gitlab-icon-scroll-down-hover,
&.scroll-bottom:hover .gitlab-icon-scroll-down {
display: none;
} }
&.sidebar-expanded { &.scroll-top:hover .gitlab-icon-scroll-up-hover,
right: #{$gutter_width + ($gl-padding * 2)}; &.scroll-bottom:hover .gitlab-icon-scroll-down-hover {
display: inline-block;
} }
a { &.scroll-top {
display: block; top: 110px;
margin-bottom: 10px; }
&.scroll-bottom {
bottom: -2px;
} }
} }
.environment-information { .autoscroll-container {
background-color: $gray-light; position: absolute;
border: 1px solid $border-color; }
padding: 12px $gl-padding;
border-radius: $border-radius-default;
svg { &.sidebar-expanded {
position: relative;
top: 1px; .scroll-link,
margin-right: 5px; .autoscroll-container {
right: ($gutter_width + ($gl-padding * 2));
} }
} }
} }
.status-message {
display: inline-block;
color: $white-light;
.status-icon {
display: inline-block;
width: 16px;
height: 33px;
}
.status-text {
float: left;
opacity: 0;
margin-right: 10px;
font-weight: normal;
line-height: 1.8;
transition: opacity 1s ease-out;
&.animate {
animation: fade-out-status 2s ease;
}
}
&:hover .status-text {
opacity: 1;
}
}
.build-header { .build-header {
position: relative; position: relative;
padding: 0; padding: 0;
...@@ -109,6 +194,15 @@ ...@@ -109,6 +194,15 @@
.bash { .bash {
display: block; display: block;
} }
.build-loader-animation {
position: relative;
width: 6px;
height: 6px;
margin: auto auto 12px 2px;
border-radius: 50%;
animation: blinking-dots 1s linear infinite;
}
} }
.right-sidebar.build-sidebar { .right-sidebar.build-sidebar {
...@@ -248,6 +342,12 @@ ...@@ -248,6 +342,12 @@
} }
} }
.build-sidebar {
.container-fluid.container-limited {
max-width: 100%;
}
}
.build-detail-row { .build-detail-row {
margin-bottom: 5px; margin-bottom: 5px;
......
...@@ -121,13 +121,6 @@ ...@@ -121,13 +121,6 @@
.folder-name { .folder-name {
cursor: pointer; cursor: pointer;
.badge {
font-weight: normal;
background-color: $gray-darker;
color: $gl-gray-light;
vertical-align: baseline;
}
} }
} }
......
...@@ -434,6 +434,7 @@ ...@@ -434,6 +434,7 @@
.issuable-meta { .issuable-meta {
display: inline-block; display: inline-block;
line-height: 18px; line-height: 18px;
font-size: 14px;
} }
.js-issuable-selector-wrap { .js-issuable-selector-wrap {
......
...@@ -21,6 +21,14 @@ ...@@ -21,6 +21,14 @@
display: inline-block; display: inline-block;
float: left; float: left;
.btn-success.dropdown-toggle .fa {
color: inherit;
}
.btn-success.dropdown-toggle:disabled {
background-color: $gl-success;
}
.accept_merge_request { .accept_merge_request {
&.ci-pending, &.ci-pending,
&.ci-running { &.ci-running {
...@@ -96,7 +104,7 @@ ...@@ -96,7 +104,7 @@
.mr-widget-body { .mr-widget-body {
h4 { h4 {
font-weight: 600; font-weight: 600;
font-size: 17px; font-size: 16px;
margin: 5px 0; margin: 5px 0;
color: $gl-gray-dark; color: $gl-gray-dark;
......
...@@ -43,7 +43,7 @@ ul.notes { ...@@ -43,7 +43,7 @@ ul.notes {
} }
.system-note-message { .system-note-message {
display: inline-block; display: inline;
&::first-letter { &::first-letter {
text-transform: lowercase; text-transform: lowercase;
...@@ -55,7 +55,7 @@ ul.notes { ...@@ -55,7 +55,7 @@ ul.notes {
} }
p { p {
display: inline-block; display: inline;
margin: 0; margin: 0;
&::first-letter { &::first-letter {
...@@ -151,6 +151,10 @@ ul.notes { ...@@ -151,6 +151,10 @@ ul.notes {
} }
} }
} }
.note-headline-light {
display: inline;
}
} }
.discussion-body { .discussion-body {
......
...@@ -80,6 +80,10 @@ ...@@ -80,6 +80,10 @@
td { td {
padding: 10px 8px; padding: 10px 8px;
} }
.commit-link {
padding: 9px 8px 10px;
}
} }
tbody { tbody {
...@@ -193,7 +197,7 @@ ...@@ -193,7 +197,7 @@
width: 8px; width: 8px;
position: absolute; position: absolute;
right: -7px; right: -7px;
bottom: 9px; bottom: 10px;
border-bottom: 2px solid $border-color; border-bottom: 2px solid $border-color;
} }
} }
...@@ -499,15 +503,10 @@ ...@@ -499,15 +503,10 @@
> .ci-action-icon-container { > .ci-action-icon-container {
position: absolute; position: absolute;
right: 4px; right: 5px;
top: 5px; top: 5px;
} }
.ci-status-icon {
position: relative;
top: 1px;
}
.ci-status-icon svg { .ci-status-icon svg {
height: 20px; height: 20px;
width: 20px; width: 20px;
...@@ -614,6 +613,10 @@ ...@@ -614,6 +613,10 @@
a { a {
display: inline-block; display: inline-block;
}
.build-content {
width: 138px;
&:hover { &:hover {
background-color: $stage-hover-bg; background-color: $stage-hover-bg;
...@@ -623,15 +626,24 @@ ...@@ -623,15 +626,24 @@
ul { ul {
max-height: 245px; max-height: 245px;
overflow: auto; overflow: auto;
margin: 5px 0; margin: 3px 0;
li { li {
padding-top: 2px; padding-top: 2px;
margin: 0 5px; margin: 4px 7px;
padding: 0 3px;
padding-left: 0; padding-left: 0;
padding-bottom: 0; padding-bottom: 0;
margin-bottom: 0; line-height: 0;
line-height: 1.2;
.ci-action-icon-container:hover {
background-color: transparent;
}
.ci-status-icon {
position: relative;
top: 2px;
}
} }
} }
} }
...@@ -680,11 +692,15 @@ ...@@ -680,11 +692,15 @@
.dropdown-build { .dropdown-build {
color: $gl-text-color-light; color: $gl-text-color-light;
.build-content {
padding: 3px 7px 6px;
}
.ci-action-icon-container { .ci-action-icon-container {
padding: 0; padding: 0;
font-size: 11px; font-size: 11px;
float: right; float: right;
margin-top: 4px; margin-top: 3px;
display: inline-block; display: inline-block;
position: relative; position: relative;
...@@ -694,16 +710,10 @@ ...@@ -694,16 +710,10 @@
} }
} }
&:hover {
background-color: $stage-hover-bg;
border-radius: 3px;
color: $gl-text-color;
}
.ci-action-icon-container { .ci-action-icon-container {
i { i {
width: 25px; width: 24px;
height: 25px; height: 24px;
&::before { &::before {
top: 1px; top: 1px;
...@@ -740,6 +750,10 @@ ...@@ -740,6 +750,10 @@
margin: 0; margin: 0;
} }
.dropdown-build .build-content {
padding: 3px 7px 7px;
}
.builds-dropdown-loading { .builds-dropdown-loading {
margin: 10px auto; margin: 10px auto;
width: 18px; width: 18px;
...@@ -788,26 +802,32 @@ ...@@ -788,26 +802,32 @@
.mini-pipeline-graph-icon-container .ci-status-icon { .mini-pipeline-graph-icon-container .ci-status-icon {
display: inline-block; display: inline-block;
border: 1px solid; border: 1px solid;
border-radius: 20px; border-radius: 22px;
margin-right: 1px; margin-right: 1px;
width: 20px; width: 22px;
height: 20px; height: 22px;
position: relative; position: relative;
z-index: 2; z-index: 2;
transition: all 0.2s cubic-bezier(0.25, 0, 1, 1); transition: all 0.2s cubic-bezier(0.25, 0, 1, 1);
svg { svg {
top: -1px; top: -1px;
left: -1px;
} }
} }
.stage-cell .mini-pipeline-graph-icon-container .ci-status-icon svg {
width: 22px;
height: 22px;
}
.builds-dropdown { .builds-dropdown {
&:focus { &:focus {
outline: none; outline: none;
margin-right: -8px; margin-right: -8px;
.ci-status-icon { .ci-status-icon {
width: 28px; width: 32px;
padding: 0 8px 0 0; padding: 0 8px 0 0;
transition: width 0.2s cubic-bezier(0.25, 0, 1, 1); transition: width 0.2s cubic-bezier(0.25, 0, 1, 1);
...@@ -851,7 +871,7 @@ ...@@ -851,7 +871,7 @@
.mini-pipeline-graph-icon-container { .mini-pipeline-graph-icon-container {
.ci-status-icon:hover, .ci-status-icon:hover,
.ci-status-icon:focus { .ci-status-icon:focus {
width: 28px; width: 32px;
padding: 0 8px 0 0; padding: 0 8px 0 0;
+ .dropdown-caret { + .dropdown-caret {
...@@ -863,7 +883,7 @@ ...@@ -863,7 +883,7 @@
font-size: 11px; font-size: 11px;
position: relative; position: relative;
top: 3px; top: 3px;
left: -11px; left: -14px;
margin-right: -6px; margin-right: -6px;
display: none; display: none;
z-index: 2; z-index: 2;
......
...@@ -929,3 +929,23 @@ a.allowed-to-push { ...@@ -929,3 +929,23 @@ a.allowed-to-push {
margin-right: 40px; margin-right: 40px;
} }
} }
.services-installation-info .row {
margin-bottom: 10px;
}
.service-installation {
padding: 32px;
margin: 32px;
border-radius: 3px;
background-color: $white-light;
h3 {
margin-top: 0;
}
hr {
margin: 32px 0;
border-color: $border-color;
}
}
.container-fluid { .container-fluid {
.ci-status { .ci-status {
padding: 2px 7px; padding: 2px 7px 4px;
margin-right: 10px; margin-right: 10px;
border: 1px solid $gray-darker; border: 1px solid $gray-darker;
white-space: nowrap; white-space: nowrap;
......
...@@ -82,6 +82,8 @@ class GroupsController < Groups::ApplicationController ...@@ -82,6 +82,8 @@ class GroupsController < Groups::ApplicationController
if Groups::UpdateService.new(@group, current_user, group_params).execute if Groups::UpdateService.new(@group, current_user, group_params).execute
redirect_to edit_group_path(@group), notice: "Group '#{@group.name}' was successfully updated." redirect_to edit_group_path(@group), notice: "Group '#{@group.name}' was successfully updated."
else else
@group.reset_path!
render action: "edit" render action: "edit"
end end
end end
......
...@@ -8,13 +8,11 @@ class Projects::CommitController < Projects::ApplicationController ...@@ -8,13 +8,11 @@ class Projects::CommitController < Projects::ApplicationController
# Authorize # Authorize
before_action :require_non_empty_project before_action :require_non_empty_project
before_action :authorize_download_code!, except: [:cancel_builds, :retry_builds] before_action :authorize_download_code!
before_action :authorize_update_build!, only: [:cancel_builds, :retry_builds]
before_action :authorize_read_pipeline!, only: [:pipelines] before_action :authorize_read_pipeline!, only: [:pipelines]
before_action :authorize_read_commit_status!, only: [:builds]
before_action :commit before_action :commit
before_action :define_commit_vars, only: [:show, :diff_for_path, :builds, :pipelines] before_action :define_commit_vars, only: [:show, :diff_for_path, :pipelines]
before_action :define_status_vars, only: [:show, :builds, :pipelines] before_action :define_status_vars, only: [:show, :pipelines]
before_action :define_note_vars, only: [:show, :diff_for_path] before_action :define_note_vars, only: [:show, :diff_for_path]
before_action :authorize_edit_tree!, only: [:revert, :cherry_pick] before_action :authorize_edit_tree!, only: [:revert, :cherry_pick]
...@@ -35,25 +33,6 @@ class Projects::CommitController < Projects::ApplicationController ...@@ -35,25 +33,6 @@ class Projects::CommitController < Projects::ApplicationController
def pipelines def pipelines
end end
def builds
end
def cancel_builds
ci_builds.running_or_pending.each(&:cancel)
redirect_back_or_default default: builds_namespace_project_commit_path(project.namespace, project, commit.sha)
end
def retry_builds
ci_builds.latest.failed.each do |build|
if build.retryable?
Ci::Build.retry(build, current_user)
end
end
redirect_back_or_default default: builds_namespace_project_commit_path(project.namespace, project, commit.sha)
end
def branches def branches
@branches = @project.repository.branch_names_contains(commit.id) @branches = @project.repository.branch_names_contains(commit.id)
@tags = @project.repository.tag_names_contains(commit.id) @tags = @project.repository.tag_names_contains(commit.id)
...@@ -98,10 +77,6 @@ class Projects::CommitController < Projects::ApplicationController ...@@ -98,10 +77,6 @@ class Projects::CommitController < Projects::ApplicationController
@noteable = @commit ||= @project.commit(params[:id]) @noteable = @commit ||= @project.commit(params[:id])
end end
def ci_builds
@ci_builds ||= Ci::Build.where(pipeline: pipelines)
end
def define_commit_vars def define_commit_vars
return git_not_found! unless commit return git_not_found! unless commit
...@@ -133,8 +108,6 @@ class Projects::CommitController < Projects::ApplicationController ...@@ -133,8 +108,6 @@ class Projects::CommitController < Projects::ApplicationController
def define_status_vars def define_status_vars
@ci_pipelines = project.pipelines.where(sha: commit.sha) @ci_pipelines = project.pipelines.where(sha: commit.sha)
@statuses = CommitStatus.where(pipeline: @ci_pipelines).relevant
@builds = Ci::Build.where(pipeline: @ci_pipelines).relevant
end end
def assign_change_commit_vars(mr_source_branch) def assign_change_commit_vars(mr_source_branch)
......
class Projects::MattermostsController < Projects::ApplicationController
include TriggersHelper
include ActionView::Helpers::AssetUrlHelper
layout 'project_settings'
before_action :authorize_admin_project!
before_action :service
before_action :teams, only: [:new]
def new
end
def create
result, message = @service.configure(current_user, configure_params)
if result
flash[:notice] = 'This service is now configured'
redirect_to edit_namespace_project_service_path(
@project.namespace, @project, service)
else
flash[:alert] = message || 'Failed to configure service'
redirect_to new_namespace_project_mattermost_path(
@project.namespace, @project)
end
end
private
def configure_params
params.require(:mattermost).permit(:trigger, :team_id).merge(
url: service_trigger_url(@service),
icon_url: asset_url('gitlab_logo.png'))
end
def teams
@teams ||= @service.list_teams(current_user)
end
def service
@service ||= @project.find_or_initialize_service('mattermost_slash_commands')
end
end
...@@ -9,11 +9,11 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -9,11 +9,11 @@ class Projects::MergeRequestsController < Projects::ApplicationController
before_action :module_enabled before_action :module_enabled
before_action :merge_request, only: [ before_action :merge_request, only: [
:edit, :update, :show, :diffs, :commits, :conflicts, :conflict_for_path, :builds, :pipelines, :merge, :merge_check, :edit, :update, :show, :diffs, :commits, :conflicts, :conflict_for_path, :pipelines, :merge, :merge_check,
:ci_status, :ci_environments_status, :toggle_subscription, :cancel_merge_when_build_succeeds, :remove_wip, :resolve_conflicts, :assign_related_issues, :ci_status, :ci_environments_status, :toggle_subscription, :cancel_merge_when_build_succeeds, :remove_wip, :resolve_conflicts, :assign_related_issues,
:approve, :rebase :approve, :rebase
] ]
before_action :validates_merge_request, only: [:show, :diffs, :commits, :builds, :pipelines] before_action :validates_merge_request, only: [:show, :diffs, :commits, :pipelines]
before_action :define_show_vars, only: [:show, :diffs, :commits, :conflicts, :conflict_for_path, :builds, :pipelines] before_action :define_show_vars, only: [:show, :diffs, :commits, :conflicts, :conflict_for_path, :builds, :pipelines]
before_action :define_widget_vars, only: [:merge, :cancel_merge_when_build_succeeds, :merge_check] before_action :define_widget_vars, only: [:merge, :cancel_merge_when_build_succeeds, :merge_check]
before_action :define_commit_vars, only: [:diffs] before_action :define_commit_vars, only: [:diffs]
...@@ -203,17 +203,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -203,17 +203,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end end
end end
def builds
respond_to do |format|
format.html do
define_discussion_vars
render 'show'
end
format.json { render json: { html: view_to_html_string('projects/merge_requests/show/_builds') } }
end
end
def pipelines def pipelines
@pipelines = @merge_request.all_pipelines @pipelines = @merge_request.all_pipelines
......
module AuthHelper module AuthHelper
PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2 facebook azure_oauth2).freeze PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2 facebook azure_oauth2 authentiq).freeze
FORM_BASED_PROVIDERS = [/\Aldap/, 'kerberos', 'crowd'].freeze FORM_BASED_PROVIDERS = [/\Aldap/, 'kerberos', 'crowd'].freeze
def ldap_enabled? def ldap_enabled?
......
...@@ -188,7 +188,7 @@ module BlobHelper ...@@ -188,7 +188,7 @@ module BlobHelper
end end
def gitlab_ci_ymls def gitlab_ci_ymls
@gitlab_ci_ymls ||= Gitlab::Template::GitlabCiYmlTemplate.dropdown_names @gitlab_ci_ymls ||= Gitlab::Template::GitlabCiYmlTemplate.dropdown_names(params[:context])
end end
def dockerfile_names def dockerfile_names
......
module CiStatusHelper module CiStatusHelper
def ci_status_path(pipeline) def ci_status_path(pipeline)
project = pipeline.project project = pipeline.project
builds_namespace_project_commit_path(project.namespace, project, pipeline.sha) namespace_project_pipeline_path(project.namespace, project, pipeline)
end end
# Is used by Commit and Merge Request Widget # Is used by Commit and Merge Request Widget
......
module MattermostHelper
def mattermost_teams_options(teams)
teams_options = teams.map do |id, options|
[options['display_name'] || options['name'], id]
end
teams_options.compact.unshift(['Select team...', '0'])
end
end
...@@ -303,13 +303,15 @@ module ProjectsHelper ...@@ -303,13 +303,15 @@ module ProjectsHelper
end end
end end
def add_special_file_path(project, file_name:, commit_message: nil) def add_special_file_path(project, file_name:, commit_message: nil, target_branch: nil, context: nil)
namespace_project_new_blob_path( namespace_project_new_blob_path(
project.namespace, project.namespace,
project, project,
project.default_branch || 'master', project.default_branch || 'master',
file_name: file_name, file_name: file_name,
commit_message: commit_message || "Add #{file_name.downcase}" commit_message: commit_message || "Add #{file_name.downcase}",
target_branch: target_branch,
context: context
) )
end end
......
...@@ -98,7 +98,7 @@ class Namespace < ActiveRecord::Base ...@@ -98,7 +98,7 @@ class Namespace < ActiveRecord::Base
def move_dir def move_dir
if any_project_has_container_registry_tags? if any_project_has_container_registry_tags?
raise Exception.new('Namespace cannot be moved, because at least one project has tags in container registry') raise Gitlab::UpdatePathError.new('Namespace cannot be moved, because at least one project has tags in container registry')
end end
# Move the namespace directory in all storages paths used by member projects # Move the namespace directory in all storages paths used by member projects
...@@ -111,7 +111,7 @@ class Namespace < ActiveRecord::Base ...@@ -111,7 +111,7 @@ class Namespace < ActiveRecord::Base
# if we cannot move namespace directory we should rollback # if we cannot move namespace directory we should rollback
# db changes in order to prevent out of sync between db and fs # db changes in order to prevent out of sync between db and fs
raise Exception.new('namespace directory cannot be moved') raise Gitlab::UpdatePathError.new('namespace directory cannot be moved')
end end
end end
......
...@@ -18,4 +18,34 @@ class MattermostSlashCommandsService < ChatSlashCommandsService ...@@ -18,4 +18,34 @@ class MattermostSlashCommandsService < ChatSlashCommandsService
def to_param def to_param
'mattermost_slash_commands' 'mattermost_slash_commands'
end end
def configure(user, params)
token = Mattermost::Command.new(user).
create(command(params))
update(active: true, token: token) if token
rescue Mattermost::Error => e
[false, e.message]
end
def list_teams(user)
Mattermost::Team.new(user).all
rescue Mattermost::Error => e
[[], e.message]
end
private
def command(params)
pretty_project_name = project.name_with_namespace
params.merge(
auto_complete: true,
auto_complete_desc: "Perform common operations on: #{pretty_project_name}",
auto_complete_hint: '[help]',
description: "Perform common operations on: #{pretty_project_name}",
display_name: "GitLab / #{pretty_project_name}",
method: 'P',
user_name: 'GitLab')
end
end end
...@@ -14,7 +14,13 @@ module Groups ...@@ -14,7 +14,13 @@ module Groups
group.assign_attributes(params) group.assign_attributes(params)
begin
group.save group.save
rescue Gitlab::UpdatePathError => e
group.errors.add(:base, e.message)
false
end
end end
end end
end end
...@@ -14,7 +14,8 @@ class ProjectPathValidator < ActiveModel::EachValidator ...@@ -14,7 +14,8 @@ class ProjectPathValidator < ActiveModel::EachValidator
# without tree as reserved name routing can match 'group/project' as group name, # without tree as reserved name routing can match 'group/project' as group name,
# 'tree' as project name and 'deploy_keys' as route. # 'tree' as project name and 'deploy_keys' as route.
# #
RESERVED = (NamespaceValidator::RESERVED + RESERVED = (NamespaceValidator::RESERVED -
%w[dashboard help ci admin search notes services] +
%w[tree commits wikis new edit create update logs_tree %w[tree commits wikis new edit create update logs_tree
preview blob blame raw files create_dir find_file]).freeze preview blob blame raw files create_dir find_file]).freeze
......
...@@ -33,7 +33,7 @@ ...@@ -33,7 +33,7 @@
= link_to admin_abuse_reports_path, title: "Abuse Reports" do = link_to admin_abuse_reports_path, title: "Abuse Reports" do
%span %span
Abuse Reports Abuse Reports
%span.badge.count= number_with_delimiter(AbuseReport.count(:all)) %span.badge.badge-dark.count= number_with_delimiter(AbuseReport.count(:all))
= nav_link(controller: :licenses) do = nav_link(controller: :licenses) do
= link_to admin_license_path, title: 'License' do = link_to admin_license_path, title: 'License' do
......
...@@ -26,13 +26,13 @@ ...@@ -26,13 +26,13 @@
%span %span
Issues Issues
- issues = IssuesFinder.new(current_user, group_id: @group.id, state: 'opened').execute - issues = IssuesFinder.new(current_user, group_id: @group.id, state: 'opened').execute
%span.badge.count= number_with_delimiter(issues.count) %span.badge.badge-dark.count= number_with_delimiter(issues.count)
= nav_link(path: 'groups#merge_requests') do = nav_link(path: 'groups#merge_requests') do
= link_to merge_requests_group_path(@group), title: 'Merge Requests' do = link_to merge_requests_group_path(@group), title: 'Merge Requests' do
%span %span
Merge Requests Merge Requests
- merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened', non_archived: true).execute - merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened', non_archived: true).execute
%span.badge.count= number_with_delimiter(merge_requests.count) %span.badge.badge-dark.count= number_with_delimiter(merge_requests.count)
= nav_link(controller: [:group_members]) do = nav_link(controller: [:group_members]) do
= link_to group_group_members_path(@group), title: 'Members' do = link_to group_group_members_path(@group), title: 'Members' do
%span %span
......
...@@ -61,14 +61,14 @@ ...@@ -61,14 +61,14 @@
%span %span
Issues Issues
- if @project.default_issues_tracker? - if @project.default_issues_tracker?
%span.badge.count.issue_counter= number_with_delimiter(IssuesFinder.new(current_user, project_id: @project.id).execute.opened.count) %span.badge.badge-dark.count.issue_counter= number_with_delimiter(IssuesFinder.new(current_user, project_id: @project.id).execute.opened.count)
- if project_nav_tab? :merge_requests - if project_nav_tab? :merge_requests
= nav_link(controller: :merge_requests) do = nav_link(controller: :merge_requests) do
= link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do
%span %span
Merge Requests Merge Requests
%span.badge.count.merge_counter= number_with_delimiter(MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened.count) %span.badge.badge-dark.count.merge_counter= number_with_delimiter(MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened.count)
- if project_nav_tab? :wiki - if project_nav_tab? :wiki
= nav_link(controller: :wikis) do = nav_link(controller: :wikis) do
......
- ref = local_assigns.fetch(:ref) - ref = local_assigns.fetch(:ref)
- status = commit.status(ref) - status = commit.status(ref)
- if status - if status
= link_to builds_namespace_project_commit_path(commit.project.namespace, commit.project, commit), class: "ci-status ci-#{status}" do = link_to pipelines_namespace_project_commit_path(commit.project.namespace, commit.project, commit), class: "ci-status ci-#{status}" do
= ci_icon_for_status(status) = ci_icon_for_status(status)
= ci_label_for_status(status) = ci_label_for_status(status)
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
= link_to pipeline_path(@build.pipeline) do = link_to pipeline_path(@build.pipeline) do
%strong ##{@build.pipeline.id} %strong ##{@build.pipeline.id}
for commit for commit
= link_to ci_status_path(@build.pipeline) do = link_to namespace_project_commit_path(@project.namespace, @project, @build.pipeline.sha) do
%strong= @build.pipeline.short_sha %strong= @build.pipeline.short_sha
from from
= link_to namespace_project_commits_path(@project.namespace, @project, @build.ref) do = link_to namespace_project_commits_path(@project.namespace, @project, @build.ref) do
......
...@@ -56,17 +56,22 @@ ...@@ -56,17 +56,22 @@
- else - else
#js-build-scroll.scroll-controls #js-build-scroll.scroll-controls
.scroll-step .scroll-step
= link_to '#build-trace', class: 'btn' do %a{ href: '#up-build-trace', id: 'scroll-top', class: 'scroll-link scroll-top', title: 'Scroll to top' }
%i.fa.fa-angle-up = custom_icon('scroll_up')
= link_to '#down-build-trace', class: 'btn' do = custom_icon('scroll_up_hover_active')
%i.fa.fa-angle-down %a{ href: '#down-build-trace', id: 'scroll-bottom', class: 'scroll-link scroll-bottom', title: 'Scroll to bottom' }
= custom_icon('scroll_down')
= custom_icon('scroll_down_hover_active')
- if @build.active? - if @build.active?
.autoscroll-container .autoscroll-container
%button.btn.btn-sm#autoscroll-button{:type => "button", :data => {:state => 'disabled'}} %span.status-message#autoscroll-status{ data: { state: 'disabled' } }
Enable autoscroll %span.status-text Autoscroll active
%i.status-icon
= custom_icon('scroll_down_hover_active')
#up-build-trace
%pre.build-trace#build-trace %pre.build-trace#build-trace
%code.bash.js-build-output %code.bash.js-build-output
= icon("refresh spin", class: "js-build-refresh") .build-loader-animation.js-build-refresh
#down-build-trace #down-build-trace
......
- @ci_pipelines.each do |pipeline|
= render "pipeline", pipeline: pipeline, pipeline_details: true
...@@ -8,7 +8,3 @@ ...@@ -8,7 +8,3 @@
= link_to pipelines_namespace_project_commit_path(@project.namespace, @project, @commit.id) do = link_to pipelines_namespace_project_commit_path(@project.namespace, @project, @commit.id) do
Pipelines Pipelines
%span.badge= @ci_pipelines.count %span.badge= @ci_pipelines.count
= nav_link(path: 'commit#builds') do
= link_to builds_namespace_project_commit_path(@project.namespace, @project, @commit.id) do
Builds
%span.badge= @statuses.count
- @no_container = true
- page_title "Builds", "#{@commit.title} (#{@commit.short_id})", "Commits"
= render "projects/commits/head"
%div{ class: container_class }
= render "commit_box"
= render "ci_menu"
= render "builds"
%p
You aren’t a member of any team on the Mattermost instance at
%strong= Gitlab.config.mattermost.host
%p
To install this service,
= link_to "#{Gitlab.config.mattermost.host}/select_team", target: '__blank' do
join a team
= icon('external-link')
and try again.
%hr
.clearfix
= link_to 'Go back', edit_namespace_project_service_path(@project.namespace, @project, @service), class: 'btn btn-lg pull-right'
%p
This service will be installed on the Mattermost instance at
%strong= link_to Gitlab.config.mattermost.host, Gitlab.config.mattermost.host
%hr
= form_for(:mattermost, method: :post, url: namespace_project_mattermost_path(@project.namespace, @project)) do |f|
%h4 Team
%p
= @teams.one? ? 'The team' : 'Select the team'
where the slash commands will be used in
- selected_id = @teams.keys.first if @teams.one?
= f.select(:team_id, mattermost_teams_options(@teams), {}, { class: 'form-control', selected: "#{selected_id}", disabled: @teams.one? })
.help-block
- if @teams.one?
This is the only team where you are an administrator.
- else
The list shows teams where you are administrator
To create a team, ask your Mattermost system administrator.
To create a team,
= link_to "#{Gitlab.config.mattermost.host}/create_team" do
use Mattermost's interface
= icon('external-link')
%hr
%h4 Command trigger word
%p Choose the word that will trigger commands
= f.text_field(:trigger, value: @project.path, class: 'form-control')
.help-block
%p
Trigger word must be unique, and can't begin with a slash or contain any spaces.
Use the word that works best for your team.
%p
Suggestions:
%code= 'gitlab'
%code= @project.path # Path contains no spaces, but dashes
%code= @project.path_with_namespace
%p
Reserved:
= link_to 'https://docs.mattermost.com/help/messaging/executing-commands.html#built-in-commands', target: '__blank' do
see list of built-in slash commands
= icon('external-link')
%hr
.clearfix
.pull-right
= link_to 'Cancel', edit_namespace_project_service_path(@project.namespace, @project, @service), class: 'btn btn-lg'
= f.submit 'Install', class: 'btn btn-save btn-lg'
.service-installation
.inline.pull-right
= custom_icon('mattermost_logo', size: 48)
%h3 Install Mattermost Command
- if @teams.empty?
= render 'no_teams'
- else
= render 'team_selection'
...@@ -34,11 +34,6 @@ ...@@ -34,11 +34,6 @@
= link_to url_for(params), data: {target: 'div#pipelines', action: 'pipelines', toggle: 'tab'} do = link_to url_for(params), data: {target: 'div#pipelines', action: 'pipelines', toggle: 'tab'} do
Pipelines Pipelines
%span.badge= @pipelines.size %span.badge= @pipelines.size
- if @pipeline.present?
%li.builds-tab
= link_to url_for(params), data: {target: 'div#builds', action: 'builds', toggle: 'tab'} do
Builds
%span.badge= @statuses_count
%li.diffs-tab %li.diffs-tab
= link_to url_for(params.merge(action: 'new_diffs')), data: {target: 'div#diffs', action: 'new/diffs', toggle: 'tab'} do = link_to url_for(params.merge(action: 'new_diffs')), data: {target: 'div#diffs', action: 'new/diffs', toggle: 'tab'} do
Changes Changes
...@@ -49,9 +44,6 @@ ...@@ -49,9 +44,6 @@
= render "projects/merge_requests/show/commits" = render "projects/merge_requests/show/commits"
#diffs.diffs.tab-pane #diffs.diffs.tab-pane
- # This tab is always loaded via AJAX - # This tab is always loaded via AJAX
- if @pipeline.present?
#builds.builds.tab-pane
= render "projects/merge_requests/show/builds"
- if @pipelines.any? - if @pipelines.any?
#pipelines.pipelines.tab-pane #pipelines.pipelines.tab-pane
= render "projects/merge_requests/show/pipelines" = render "projects/merge_requests/show/pipelines"
...@@ -66,6 +58,5 @@ ...@@ -66,6 +58,5 @@
}); });
:javascript :javascript
var merge_request = new MergeRequest({ var merge_request = new MergeRequest({
action: "#{(@show_changes_tab ? 'new/diffs' : 'new')}", action: "#{(@show_changes_tab ? 'new/diffs' : 'new')}"
buildsLoaded: "#{@pipeline.present? ? 'true' : 'false'}"
}); });
...@@ -66,11 +66,6 @@ ...@@ -66,11 +66,6 @@
= link_to pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: '#pipelines', action: 'pipelines', toggle: 'tab' } do = link_to pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: '#pipelines', action: 'pipelines', toggle: 'tab' } do
Pipelines Pipelines
%span.badge= @pipelines.size %span.badge= @pipelines.size
- if @pipeline.present?
%li.builds-tab
= link_to builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: '#builds', action: 'builds', toggle: 'tab' } do
Builds
%span.badge= @statuses_count
%li.diffs-tab %li.diffs-tab
= link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#diffs', action: 'diffs', toggle: 'tab' } do = link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#diffs', action: 'diffs', toggle: 'tab' } do
Changes Changes
...@@ -99,8 +94,6 @@ ...@@ -99,8 +94,6 @@
#commits.commits.tab-pane #commits.commits.tab-pane
- # This tab is always loaded via AJAX - # This tab is always loaded via AJAX
#builds.builds.tab-pane
- # This tab is always loaded via AJAX
#pipelines.pipelines.tab-pane #pipelines.pipelines.tab-pane
- # This tab is always loaded via AJAX - # This tab is always loaded via AJAX
#diffs.diffs.tab-pane #diffs.diffs.tab-pane
......
= render "projects/commit/pipeline", pipeline: @pipeline, link_to_commit: true
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
- # TODO, remove in later versions when services like Jenkins will set CI status via Commit status API - # TODO, remove in later versions when services like Jenkins will set CI status via Commit status API
.mr-widget-heading .mr-widget-heading
- %w[success skipped canceled failed running pending].each do |status| - %w[success skipped canceled failed running pending].each do |status|
.ci_widget{class: "ci-#{status}", style: "display:none"} .ci_widget{class: "ci-#{status} ci-status-icon-#{status}", style: "display:none"}
= ci_icon_for_status(status) = ci_icon_for_status(status)
%span %span
CI build CI build
......
...@@ -24,12 +24,10 @@ ...@@ -24,12 +24,10 @@
preparing: "{{status}} build", preparing: "{{status}} build",
normal: "Build {{status}}" normal: "Build {{status}}"
}, },
builds_path: "#{builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}",
pipelines_path: "#{pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}" pipelines_path: "#{pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}"
}; };
if (typeof merge_request_widget !== 'undefined') { if (typeof merge_request_widget !== 'undefined') {
clearInterval(merge_request_widget.fetchBuildStatusInterval);
merge_request_widget.cancelPolling(); merge_request_widget.cancelPolling();
merge_request_widget.clearEventListeners(); merge_request_widget.clearEventListeners();
} }
......
- content_for :page_specific_javascripts do
= page_specific_javascript_tag('merge_request_widget/ci_bundle.js')
- status_class = @pipeline ? " ci-#{@pipeline.status}" : nil - status_class = @pipeline ? " ci-#{@pipeline.status}" : nil
= form_for [:merge, @project.namespace.becomes(Namespace), @project, @merge_request], remote: true, method: :post, html: { class: 'accept-mr-form js-quick-submit js-requires-input' } do |f| = form_for [:merge, @project.namespace.becomes(Namespace), @project, @merge_request], remote: true, method: :post, html: { class: 'accept-mr-form js-quick-submit js-requires-input' } do |f|
...@@ -53,21 +56,3 @@ ...@@ -53,21 +56,3 @@
rows: 14, hint: true rows: 14, hint: true
= hidden_field_tag :merge_when_build_succeeds, "", autocomplete: "off" = hidden_field_tag :merge_when_build_succeeds, "", autocomplete: "off"
:javascript
$('.accept-mr-form').on('ajax:send', function() {
$(".accept-mr-form :input").disable();
});
$('.accept_merge_request').on('click', function() {
$('.js-merge-button').html("<i class='fa fa-spinner fa-spin'></i> Merge in progress");
});
$('.merge_when_build_succeeds').on('click', function() {
$("#merge_when_build_succeeds").val("1");
});
$('.js-merge-dropdown a').on('click', function(e) {
e.preventDefault();
$(this).closest("form").submit();
});
- content_for :page_specific_javascripts do
= page_specific_javascript_tag('merge_request_widget/ci_bundle.js')
%strong %strong
= icon("spinner spin") = icon("spinner spin")
Checking ability to merge automatically&hellip; Checking ability to merge automatically&hellip;
:javascript
$(function() {
merge_request_widget.getMergeStatus();
});
...@@ -8,7 +8,6 @@ ...@@ -8,7 +8,6 @@
.col-lg-9 .col-lg-9
= form_for(@service, as: :service, url: namespace_project_service_path(@project.namespace, @project, @service.to_param), method: :put, html: { class: 'form-horizontal' }) do |form| = form_for(@service, as: :service, url: namespace_project_service_path(@project.namespace, @project, @service.to_param), method: :put, html: { class: 'form-horizontal' }) do |form|
= render 'shared/service_settings', form: form, subject: @service = render 'shared/service_settings', form: form, subject: @service
.footer-block.row-content-block .footer-block.row-content-block
= form.submit 'Save changes', class: 'btn btn-save' = form.submit 'Save changes', class: 'btn btn-save'
&nbsp; &nbsp;
......
- run_actions_text = "Perform common operations on this project: #{@project.name_with_namespace}"
To setup this service:
%ul.list-unstyled
%li
1.
= link_to 'Enable custom slash commands', 'https://docs.mattermost.com/developer/slash-commands.html#enabling-custom-commands'
on your Mattermost installation
%li
2.
= link_to 'Add a slash command', 'https://docs.mattermost.com/developer/slash-commands.html#set-up-a-custom-command'
in Mattermost with these options:
%hr
.help-form
.form-group
= label_tag :display_name, 'Display name', class: 'col-sm-2 col-xs-12 control-label'
.col-sm-10.col-xs-12.input-group
= text_field_tag :display_name, "GitLab / #{@project.name_with_namespace}", class: 'form-control input-sm', readonly: 'readonly'
.input-group-btn
= clipboard_button(clipboard_target: '#display_name')
.form-group
= label_tag :description, 'Description', class: 'col-sm-2 col-xs-12 control-label'
.col-sm-10.col-xs-12.input-group
= text_field_tag :description, run_actions_text, class: 'form-control input-sm', readonly: 'readonly'
.input-group-btn
= clipboard_button(clipboard_target: '#description')
.form-group
= label_tag nil, 'Command trigger word', class: 'col-sm-2 col-xs-12 control-label'
.col-sm-10.col-xs-12.text-block
%p Fill in the word that works best for your team.
%p
Suggestions:
%code= 'gitlab'
%code= @project.path # Path contains no spaces, but dashes
%code= @project.path_with_namespace
.form-group
= label_tag :request_url, 'Request URL', class: 'col-sm-2 col-xs-12 control-label'
.col-sm-10.col-xs-12.input-group
= text_field_tag :request_url, service_trigger_url(subject), class: 'form-control input-sm', readonly: 'readonly'
.input-group-btn
= clipboard_button(clipboard_target: '#request_url')
.form-group
= label_tag nil, 'Request method', class: 'col-sm-2 col-xs-12 control-label'
.col-sm-10.col-xs-12.text-block POST
.form-group
= label_tag :response_username, 'Response username', class: 'col-sm-2 col-xs-12 control-label'
.col-sm-10.col-xs-12.input-group
= text_field_tag :response_username, 'GitLab', class: 'form-control input-sm', readonly: 'readonly'
.input-group-btn
= clipboard_button(clipboard_target: '#response_username')
.form-group
= label_tag :response_icon, 'Response icon', class: 'col-sm-2 col-xs-12 control-label'
.col-sm-10.col-xs-12.input-group
= text_field_tag :response_icon, asset_url('gitlab_logo.png'), class: 'form-control input-sm', readonly: 'readonly'
.input-group-btn
= clipboard_button(clipboard_target: '#response_icon')
.form-group
= label_tag nil, 'Autocomplete', class: 'col-sm-2 col-xs-12 control-label'
.col-sm-10.col-xs-12.text-block Yes
.form-group
= label_tag :autocomplete_hint, 'Autocomplete hint', class: 'col-sm-2 col-xs-12 control-label'
.col-sm-10.col-xs-12.input-group
= text_field_tag :autocomplete_hint, '[help]', class: 'form-control input-sm', readonly: 'readonly'
.input-group-btn
= clipboard_button(clipboard_target: '#autocomplete_hint')
.form-group
= label_tag :autocomplete_description, 'Autocomplete description', class: 'col-sm-2 col-xs-12 control-label'
.col-sm-10.col-xs-12.input-group
= text_field_tag :autocomplete_description, run_actions_text, class: 'form-control input-sm', readonly: 'readonly'
.input-group-btn
= clipboard_button(clipboard_target: '#autocomplete_description')
%hr
%ul.list-unstyled
%li
3. After adding the slash command, paste the
%strong token
into the field below
- run_actions_text = "Perform common operations on this project: #{@project.name_with_namespace}" - enabled = Gitlab.config.mattermost.enabled
.well .well
This service allows GitLab users to perform common operations on this This service allows GitLab users to perform common operations on this
...@@ -7,93 +7,9 @@ ...@@ -7,93 +7,9 @@
See list of available commands in Mattermost after setting up this service, See list of available commands in Mattermost after setting up this service,
by entering by entering
%code /&lt;command_trigger_word&gt; help %code /&lt;command_trigger_word&gt; help
%br
%br
To setup this service:
%ul.list-unstyled
%li
1.
= link_to 'Enable custom slash commands', 'https://docs.mattermost.com/developer/slash-commands.html#enabling-custom-commands'
on your Mattermost installation
%li
2.
= link_to 'Add a slash command', 'https://docs.mattermost.com/developer/slash-commands.html#set-up-a-custom-command'
in Mattermost with these options:
%hr
.help-form
.form-group
= label_tag :display_name, 'Display name', class: 'col-sm-2 col-xs-12 control-label'
.col-sm-10.col-xs-12.input-group
= text_field_tag :display_name, "GitLab / #{@project.name_with_namespace}", class: 'form-control input-sm', readonly: 'readonly'
.input-group-btn
= clipboard_button(clipboard_target: '#display_name')
.form-group
= label_tag :description, 'Description', class: 'col-sm-2 col-xs-12 control-label'
.col-sm-10.col-xs-12.input-group
= text_field_tag :description, run_actions_text, class: 'form-control input-sm', readonly: 'readonly'
.input-group-btn
= clipboard_button(clipboard_target: '#description')
.form-group
= label_tag nil, 'Command trigger word', class: 'col-sm-2 col-xs-12 control-label'
.col-sm-10.col-xs-12.text-block
%p Fill in the word that works best for your team.
%p
Suggestions:
%code= 'gitlab'
%code= @project.path # Path contains no spaces, but dashes
%code= @project.path_with_namespace
.form-group
= label_tag :request_url, 'Request URL', class: 'col-sm-2 col-xs-12 control-label'
.col-sm-10.col-xs-12.input-group
= text_field_tag :request_url, service_trigger_url(subject), class: 'form-control input-sm', readonly: 'readonly'
.input-group-btn
= clipboard_button(clipboard_target: '#request_url')
.form-group
= label_tag nil, 'Request method', class: 'col-sm-2 col-xs-12 control-label'
.col-sm-10.col-xs-12.text-block POST
.form-group
= label_tag :response_username, 'Response username', class: 'col-sm-2 col-xs-12 control-label'
.col-sm-10.col-xs-12.input-group
= text_field_tag :response_username, 'GitLab', class: 'form-control input-sm', readonly: 'readonly'
.input-group-btn
= clipboard_button(clipboard_target: '#response_username')
.form-group
= label_tag :response_icon, 'Response icon', class: 'col-sm-2 col-xs-12 control-label'
.col-sm-10.col-xs-12.input-group
= text_field_tag :response_icon, asset_url('gitlab_logo.png'), class: 'form-control input-sm', readonly: 'readonly'
.input-group-btn
= clipboard_button(clipboard_target: '#response_icon')
.form-group
= label_tag nil, 'Autocomplete', class: 'col-sm-2 col-xs-12 control-label'
.col-sm-10.col-xs-12.text-block Yes
.form-group
= label_tag :autocomplete_hint, 'Autocomplete hint', class: 'col-sm-2 col-xs-12 control-label'
.col-sm-10.col-xs-12.input-group
= text_field_tag :autocomplete_hint, '[help]', class: 'form-control input-sm', readonly: 'readonly'
.input-group-btn
= clipboard_button(clipboard_target: '#autocomplete_hint')
.form-group
= label_tag :autocomplete_description, 'Autocomplete description', class: 'col-sm-2 col-xs-12 control-label'
.col-sm-10.col-xs-12.input-group
= text_field_tag :autocomplete_description, run_actions_text, class: 'form-control input-sm', readonly: 'readonly'
.input-group-btn
= clipboard_button(clipboard_target: '#autocomplete_description')
%hr - unless enabled
= render 'projects/services/mattermost_slash_commands/detailed_help', subject: @service
%ul.list-unstyled - if enabled
%li = render 'projects/services/mattermost_slash_commands/installation_info', subject: @service
3. After adding the slash command, paste the
%strong token
into the field below
.services-installation-info
- unless @service.activated?
.row
.col-sm-9.col-sm-offset-3
= link_to new_namespace_project_mattermost_path(@project.namespace, @project), class: 'btn btn-lg' do
= custom_icon('mattermost_logo', size: 15)
= 'Add to Mattermost'
...@@ -70,6 +70,10 @@ ...@@ -70,6 +70,10 @@
- if koding_enabled? && @repository.koding_yml.blank? - if koding_enabled? && @repository.koding_yml.blank?
%li.missing %li.missing
= link_to 'Set up Koding', add_koding_stack_path(@project) = link_to 'Set up Koding', add_koding_stack_path(@project)
- if @repository.gitlab_ci_yml.blank? && @project.deployment_service.present?
%li.missing
= link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml', commit_message: 'Set up autodeploy', target_branch: 'autodeploy', context: 'autodeploy') do
Set up autodeploy
- if @repository.commit - if @repository.commit
.project-last-commit{ class: container_class } .project-last-commit{ class: container_class }
......
...@@ -10,10 +10,10 @@ ...@@ -10,10 +10,10 @@
.text-content .text-content
- if has_button && current_user - if has_button && current_user
%h4 %h4
The Issue Tracker is a good place to add things that need to be improved or solved in a project! The Issue Tracker is the place to add things that need to be improved or solved in a project
%p %p
An issue can be a bug, a todo or a feature request that needs to be discussed in a project. Issues can be bugs, tasks or ideas to be discussed.
Besides, issues are searchable and filterable. Also, issues are searchable and filterable.
- if project_select_button - if project_select_button
= render 'shared/new_project_item_select', path: 'issues/new', label: 'New issue' = render 'shared/new_project_item_select', path: 'issues/new', label: 'New issue'
- else - else
......
<svg xmlns="http://www.w3.org/2000/svg" width="<%= size %>" height="<%= size %>" version="1" viewBox="0 0 501 501"><path d="M236 .7C137.7 7.5 54 68.2 18.2 158.5c-32 81-19.6 172.8 33 242.5 39.8 53 97.2 87 164.3 97 16.5 2.7 48 3.2 63.5 1.2 48.7-6.3 92.2-24.6 129-54.2 13-10.5 33-31.2 42.2-43.7 26.4-35.5 42.8-75.8 49-120.3 1.6-12.3 1.6-48.7 0-61-4-28.3-12-54.8-24.2-79.5-12.8-26-26.5-45.3-46.8-65.8C417.8 64 400.2 49 398.4 49c-.6 0-.4 10.5.3 26l1.3 26 7 8.7c19 23.7 32.8 53.5 38.2 83 2.5 14 3 43 1 55.8-4.5 27.8-15.2 54-31 76.5-8.6 12.2-28 31.6-40.2 40.2-24 17-50 27.6-80 33-10 1.8-49 1.8-59 0-43-7.7-78.8-26-107.2-54.8-29.3-29.7-46.5-64-52.4-104.4-2-14-1.5-42 1-55C90 121.4 132 72 192 49.7c8-3 18.4-5.8 29.5-8.2 1.7-.4 34.4-38 35.3-40.6.3-1-10.2-1-20.8-.4z"/><path d="M322.2 24.6c-1.3.8-8.4 9.3-16 18.7-7.4 9.5-22.4 28-33.2 41.2-51 62.2-66 81.6-70.6 91-6 12-8.4 21-9 33-1.2 19.8 5 36 19 50C222 268 230 273 243 277.2c9 3 10.4 3.2 24 3.2 13.8 0 15 0 22.6-3 23.2-9 39-28.4 45-55.7 2-8.2 2-28.7.4-79.7l-2-72c-1-36.8-1.4-41.8-3-44-2-3-4.8-3.6-7.8-1.4z"/></svg>
<svg width="16" height="33" class="gitlab-icon-scroll-down" viewBox="0 0 16 33" xmlns="http://www.w3.org/2000/svg">
<path fill="#ffffff" d="M1.385 5.534v12.47a4.145 4.145 0 0 0 4.144 4.15h4.942a4.151 4.151 0 0 0 4.144-4.15V5.535a4.145 4.145 0 0 0-4.144-4.15H5.53a4.151 4.151 0 0 0-4.144 4.15zM8.88 30.27v-4.351a.688.688 0 0 0-.69-.688.687.687 0 0 0-.69.688v4.334l-1.345-1.346a.69.69 0 0 0-.976.976l2.526 2.526a.685.685 0 0 0 .494.2.685.685 0 0 0 .493-.2l2.526-2.526a.69.69 0 1 0-.976-.976L8.88 30.27zM0 5.534A5.536 5.536 0 0 1 5.529 0h4.942A5.53 5.53 0 0 1 16 5.534v12.47a5.536 5.536 0 0 1-5.529 5.534H5.53A5.53 5.53 0 0 1 0 18.005V5.534zm7 1.01a1 1 0 1 1 2 0v2.143a1 1 0 1 1-2 0V6.544z" fill-rule="evenodd"/>
</svg>
<svg width="16" height="33" class="gitlab-icon-scroll-down-hover" viewBox="0 0 16 33" xmlns="http://www.w3.org/2000/svg">
<path fill="#ffffff" d="M8.88 30.27v-4.351a.688.688 0 0 0-.69-.688.687.687 0 0 0-.69.688v4.334l-1.345-1.346a.69.69 0 0 0-.976.976l2.526 2.526a.685.685 0 0 0 .494.2.685.685 0 0 0 .493-.2l2.526-2.526a.69.69 0 1 0-.976-.976L8.88 30.27zM0 5.534A5.536 5.536 0 0 1 5.529 0h4.942A5.53 5.53 0 0 1 16 5.534v12.47a5.536 5.536 0 0 1-5.529 5.534H5.53A5.53 5.53 0 0 1 0 18.005V5.534zm7 1.01a1 1 0 1 1 2 0v2.143a1 1 0 1 1-2 0V6.544z" fill-rule="evenodd"/>
</svg>
<svg width="16" height="33" class="gitlab-icon-scroll-up" viewBox="0 0 16 33" xmlns="http://www.w3.org/2000/svg">
<path fill="#ffffff" d="M1.385 14.534v12.47a4.145 4.145 0 0 0 4.144 4.15h4.942a4.151 4.151 0 0 0 4.144-4.15v-12.47a4.145 4.145 0 0 0-4.144-4.15H5.53a4.151 4.151 0 0 0-4.144 4.15zM8.88 2.609V6.96a.688.688 0 0 1-.69.688.687.687 0 0 1-.69-.688V2.627L6.155 3.972a.69.69 0 0 1-.976-.976L7.705.47a.685.685 0 0 1 .494-.2.685.685 0 0 1 .493.2l2.526 2.526a.69.69 0 1 1-.976.976L8.88 2.609zM0 14.534A5.536 5.536 0 0 1 5.529 9h4.942A5.53 5.53 0 0 1 16 14.534v12.47a5.536 5.536 0 0 1-5.529 5.534H5.53A5.53 5.53 0 0 1 0 27.005V14.534zm7 1.01a1 1 0 1 1 2 0v2.143a1 1 0 1 1-2 0v-2.143z" fill-rule="evenodd"/>
</svg>
<svg width="16" height="33" class="gitlab-icon-scroll-up-hover" viewBox="0 0 16 33" xmlns="http://www.w3.org/2000/svg">
<path fill="#ffffff" d="M8.88 2.646l1.362 1.362a.69.69 0 0 0 .976-.976L8.692.507A.685.685 0 0 0 8.2.306a.685.685 0 0 0-.494.2L5.179 3.033a.69.69 0 1 0 .976.976L7.5 2.663v4.179c0 .38.306.688.69.688.381 0 .69-.306.69-.688V2.646zM0 14.534A5.536 5.536 0 0 1 5.529 9h4.942A5.53 5.53 0 0 1 16 14.534v12.47a5.536 5.536 0 0 1-5.529 5.534H5.53A5.53 5.53 0 0 1 0 27.005V14.534zm7 1.01a1 1 0 1 1 2 0v2.143a1 1 0 1 1-2 0v-2.143z" fill-rule="evenodd"/>
</svg>
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
- show_menu_above = show_menu_above || false - show_menu_above = show_menu_above || false
- selected_text = selected.try(:title) || params[:milestone_title] - selected_text = selected.try(:title) || params[:milestone_title]
- dropdown_title = local_assigns.fetch(:dropdown_title, "Filter by milestone") - dropdown_title = local_assigns.fetch(:dropdown_title, "Filter by milestone")
- if selected.present? - if selected.present? || params[:milestone_title].present?
= hidden_field_tag(name, name == :milestone_title ? selected_text : selected.id) = hidden_field_tag(name, name == :milestone_title ? selected_text : selected.id)
= dropdown_tag(milestone_dropdown_label(selected_text), options: { title: dropdown_title, toggle_class: "js-milestone-select js-filter-submit #{extra_class}", filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", = dropdown_tag(milestone_dropdown_label(selected_text), options: { title: dropdown_title, toggle_class: "js-milestone-select js-filter-submit #{extra_class}", filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone",
placeholder: "Search milestones", footer_content: project.present?, data: { show_no: true, show_menu_above: show_menu_above, show_any: show_any, show_upcoming: show_upcoming, field_name: name, selected: selected.try(:title), project_id: project.try(:id), milestones: milestones_filter_dropdown_path, default_label: "Milestone" } }) do placeholder: "Search milestones", footer_content: project.present?, data: { show_no: true, show_menu_above: show_menu_above, show_any: show_any, show_upcoming: show_upcoming, field_name: name, selected: selected.try(:title), project_id: project.try(:id), milestones: milestones_filter_dropdown_path, default_label: "Milestone" } }) do
......
---
title: Improve Build Log scrolling experience
merge_request: 7895
author:
---
title: 'Filter protocol-relative URLs in ExternalLinkFilter. Fixes issue #22742'
merge_request: 6635
author: Makoto Scott-Hinkle
---
title: Resolve "Remove Builds tab from Merge Requests and Commits"
merge_request: 7763
author:
---
title: Fix lookup of project by unknown ref when caching is enabled
merge_request: 7988
author:
---
title: Fixes left align issue for long system notes
merge_request: 7982
author:
---
title: Ensure nil User-Agent doesn't break the CI API
merge_request:
author:
---
title: Adds CSS class to status icon on MR widget to prevent non-colored icon
merge_request: 8219
author:
---
title: Adds background color for disabled state to merge when succeeds dropdown
merge_request: 8222
author:
---
title: Standardises font-size for titles in Issues, Merge Requests and Merge Request widget
merge_request: 8235
author:
---
title: Use Grape's new Route methods
merge_request:
author:
---
title: Adds back CSS for progress-bars
merge_request: 8237
author:
---
title: Add Authentiq as Oauth provider
merge_request: 8038
author: Alexandros Keramidas
---
title: Introduce "Set up autodeploy" button to help configure GitLab CI for deployment
merge_request: 8135
author:
---
title: Added lighter count badge background-color for on white backgrounds
merge_request: 7873
author:
---
title: Rename groups with .git in the end of the path
merge_request: 8199
author:
---
title: Allow projects with 'dashboard' as path
merge_request:
author:
---
title: 'Whitelist next project names: notes, services'
merge_request:
author:
---
title: 'Whitelist next project names: help, ci, admin, search'
merge_request: 8227
author:
---
title: Improve copy in Issue Tracker empty state
merge_request: 8202
author:
---
title: Fix 500 error renaming group
merge_request:
author:
---
title: Allow to auto-configure Mattermost
merge_request: 8070
author:
---
title: Remove unused and void services from the database
merge_request:
author:
...@@ -86,6 +86,7 @@ module Gitlab ...@@ -86,6 +86,7 @@ module Gitlab
# Enable the asset pipeline # Enable the asset pipeline
config.assets.enabled = true config.assets.enabled = true
config.assets.paths << Gemojione.images_path config.assets.paths << Gemojione.images_path
config.assets.paths << "vendor/assets/fonts"
config.assets.precompile << "*.png" config.assets.precompile << "*.png"
config.assets.precompile << "print.css" config.assets.precompile << "print.css"
config.assets.precompile << "notify.css" config.assets.precompile << "notify.css"
...@@ -101,6 +102,7 @@ module Gitlab ...@@ -101,6 +102,7 @@ module Gitlab
config.assets.precompile << "protected_branches/protected_branches_bundle.js" config.assets.precompile << "protected_branches/protected_branches_bundle.js"
config.assets.precompile << "diff_notes/diff_notes_bundle.js" config.assets.precompile << "diff_notes/diff_notes_bundle.js"
config.assets.precompile << "issuable/issuable_bundle.js" config.assets.precompile << "issuable/issuable_bundle.js"
config.assets.precompile << "merge_request_widget/ci_bundle.js"
config.assets.precompile << "boards/boards_bundle.js" config.assets.precompile << "boards/boards_bundle.js"
config.assets.precompile << "cycle_analytics/cycle_analytics_bundle.js" config.assets.precompile << "cycle_analytics/cycle_analytics_bundle.js"
config.assets.precompile << "merge_conflicts/merge_conflicts_bundle.js" config.assets.precompile << "merge_conflicts/merge_conflicts_bundle.js"
...@@ -112,6 +114,7 @@ module Gitlab ...@@ -112,6 +114,7 @@ module Gitlab
config.assets.precompile << "lib/utils/*.js" config.assets.precompile << "lib/utils/*.js"
config.assets.precompile << "lib/*.js" config.assets.precompile << "lib/*.js"
config.assets.precompile << "u2f.js" config.assets.precompile << "u2f.js"
config.assets.precompile << "vendor/assets/fonts/*"
# Version of your assets, change this if you want to expire all your assets # Version of your assets, change this if you want to expire all your assets
config.assets.version = '1.0' config.assets.version = '1.0'
......
...@@ -468,6 +468,16 @@ production: &base ...@@ -468,6 +468,16 @@ production: &base
# login_url: '/cas/login', # login_url: '/cas/login',
# service_validate_url: '/cas/p3/serviceValidate', # service_validate_url: '/cas/p3/serviceValidate',
# logout_url: '/cas/logout'} } # logout_url: '/cas/logout'} }
# - { name: 'authentiq',
# # for client credentials (client ID and secret), go to https://www.authentiq.com/
# app_id: 'YOUR_CLIENT_ID',
# app_secret: 'YOUR_CLIENT_SECRET',
# args: {
# scope: 'aq:name email~rs address aq:push'
# # redirect_uri parameter is optional except when 'gitlab.host' in this file is set to 'localhost'
# # redirect_uri: 'YOUR_REDIRECT_URI'
# }
# }
# - { name: 'github', # - { name: 'github',
# app_id: 'YOUR_APP_ID', # app_id: 'YOUR_APP_ID',
# app_secret: 'YOUR_APP_SECRET', # app_secret: 'YOUR_APP_SECRET',
......
...@@ -32,10 +32,7 @@ constraints(ProjectUrlConstrainer.new) do ...@@ -32,10 +32,7 @@ constraints(ProjectUrlConstrainer.new) do
resources :commit, only: [:show], constraints: { id: /\h{7,40}/ } do resources :commit, only: [:show], constraints: { id: /\h{7,40}/ } do
member do member do
get :branches get :branches
get :builds
get :pipelines get :pipelines
post :cancel_builds
post :retry_builds
post :revert post :revert
post :cherry_pick post :cherry_pick
get :diff_for_path get :diff_for_path
...@@ -82,6 +79,8 @@ constraints(ProjectUrlConstrainer.new) do ...@@ -82,6 +79,8 @@ constraints(ProjectUrlConstrainer.new) do
end end
end end
resource :mattermost, only: [:new, :create]
resources :deploy_keys, constraints: { id: /\d+/ }, only: [:index, :new, :create] do resources :deploy_keys, constraints: { id: /\d+/ }, only: [:index, :new, :create] do
member do member do
put :enable put :enable
...@@ -98,7 +97,6 @@ constraints(ProjectUrlConstrainer.new) do ...@@ -98,7 +97,6 @@ constraints(ProjectUrlConstrainer.new) do
get :diffs get :diffs
get :conflicts get :conflicts
get :conflict_for_path get :conflict_for_path
get :builds
get :pipelines get :pipelines
get :merge_check get :merge_check
post :merge post :merge
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class RemoveDotGitFromGroupNames < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
include Gitlab::ShellAdapter
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
def up
invalid_groups.each do |group|
path_was = group['path']
path_was_wildcard = quote_string("#{path_was}/%")
path = quote_string(rename_path(path_was))
move_namespace(group['id'], path_was, path)
execute "UPDATE routes SET path = '#{path}' WHERE source_type = 'Namespace' AND source_id = #{group['id']}"
execute "UPDATE namespaces SET path = '#{path}' WHERE id = #{group['id']}"
select_all("SELECT id, path FROM routes WHERE path LIKE '#{path_was_wildcard}'").each do |route|
new_path = "#{path}/#{route['path'].split('/').last}"
execute "UPDATE routes SET path = '#{new_path}' WHERE id = #{route['id']}"
end
end
end
def down
# nothing to do here
end
private
def invalid_groups
select_all("SELECT id, path FROM namespaces WHERE type = 'Group' AND path LIKE '%.git'")
end
def route_exists?(path)
select_all("SELECT id, path FROM routes WHERE path = '#{quote_string(path)}'").present?
end
# Accepts invalid path like test.git and returns test_git or
# test_git1 if test_git already taken
def rename_path(path)
# To stay closer with original name and reduce risk of duplicates
# we rename suffix instead of removing it
path = path.sub(/\.git\z/, '_git')
counter = 0
base = path
while route_exists?(path)
counter += 1
path = "#{base}#{counter}"
end
path
end
def move_namespace(group_id, path_was, path)
repository_storage_paths = select_all("SELECT distinct(repository_storage) FROM projects WHERE namespace_id = #{group_id}").map do |row|
Gitlab.config.repositories.storages[row['repository_storage']]
end.compact
# Move the namespace directory in all storages paths used by member projects
repository_storage_paths.each do |repository_storage_path|
# Ensure old directory exists before moving it
gitlab_shell.add_namespace(repository_storage_path, path_was)
unless gitlab_shell.mv_namespace(repository_storage_path, path_was, path)
Rails.logger.error "Exception moving path #{repository_storage_path} from #{path_was} to #{path}"
# if we cannot move namespace directory we should rollback
# db changes in order to prevent out of sync between db and fs
raise Exception.new('namespace directory cannot be moved')
end
end
Gitlab::UploadsTransfer.new.rename_namespace(path_was, path)
end
end
class RemoveUnneededServices < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def up
execute("DELETE FROM services WHERE active = false AND properties = '{}';")
end
def down
# noop
end
end
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20161213172958) do ActiveRecord::Schema.define(version: 20161221140236) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
......
...@@ -6,7 +6,7 @@ providers. ...@@ -6,7 +6,7 @@ providers.
- [LDAP](ldap.md) Includes Active Directory, Apple Open Directory, Open LDAP, - [LDAP](ldap.md) Includes Active Directory, Apple Open Directory, Open LDAP,
and 389 Server and 389 Server
- [OmniAuth](../../integration/omniauth.md) Sign in via Twitter, GitHub, GitLab.com, Google, - [OmniAuth](../../integration/omniauth.md) Sign in via Twitter, GitHub, GitLab.com, Google,
Bitbucket, Facebook, Shibboleth, Crowd and Azure Bitbucket, Facebook, Shibboleth, Crowd, Azure and Authentiq ID
- [CAS](../../integration/cas.md) Configure GitLab to sign in using CAS - [CAS](../../integration/cas.md) Configure GitLab to sign in using CAS
- [SAML](../../integration/saml.md) Configure GitLab as a SAML 2.0 Service Provider - [SAML](../../integration/saml.md) Configure GitLab as a SAML 2.0 Service Provider
- [Okta](okta.md) Configure GitLab to sign in using Okta - [Okta](okta.md) Configure GitLab to sign in using Okta
# Authentiq OmniAuth Provider
To enable the Authentiq OmniAuth provider for passwordless authentication you must register an application with Authentiq.
Authentiq will generate a Client ID and the accompanying Client Secret for you to use.
1. Get your Client credentials (Client ID and Client Secret) at [Authentiq](https://www.authentiq.com/register).
2. On your GitLab server, open the configuration file:
For omnibus installation
```sh
sudo editor /etc/gitlab/gitlab.rb
```
For installations from source:
```sh
sudo -u git -H editor /home/git/gitlab/config/gitlab.yml
```
3. See [Initial OmniAuth Configuration](../../integration/omniauth.md#initial-omniauth-configuration) for initial settings to enable single sign-on and add Authentiq as an OAuth provider.
4. Add the provider configuration for Authentiq:
For Omnibus packages:
```ruby
gitlab_rails['omniauth_providers'] = [
{
"name" => "authentiq",
"app_id" => "YOUR_CLIENT_ID",
"app_secret" => "YOUR_CLIENT_SECRET",
"args" => {
scope: 'aq:name email~rs aq:push'
}
}
]
```
For installations from source:
```yaml
- { name: 'authentiq',
app_id: 'YOUR_CLIENT_ID',
app_secret: 'YOUR_CLIENT_SECRET',
args: {
scope: 'aq:name email~rs aq:push'
}
}
```
5. The `scope` is set to request the user's name, email (required and signed), and permission to send push notifications to sign in on subsequent visits.
See [OmniAuth Authentiq strategy](https://github.com/AuthentiqID/omniauth-authentiq#scopes-and-redirect-uri-configuration) for more information on scopes and modifiers.
6. Change 'YOUR_CLIENT_ID' and 'YOUR_CLIENT_SECRET' to the Client credentials you received in step 1.
7. Save the configuration file.
8. [Reconfigure](../restart_gitlab.md#omnibus-gitlab-reconfigure) or [restart GitLab](../restart_gitlab.md#installations-from-source)
for the changes to take effect if you installed GitLab via Omnibus or from source respectively.
On the sign in page there should now be an Authentiq icon below the regular sign in form. Click the icon to begin the authentication process.
- If the user has the Authentiq ID app installed in their iOS or Android device, they can scan the QR code, decide what personal details to share and sign in to your GitLab installation.
- If not they will be prompted to download the app and then follow the procedure above.
If everything goes right, the user will be returned to GitLab and will be signed in.
\ No newline at end of file
...@@ -23,6 +23,7 @@ ...@@ -23,6 +23,7 @@
- [CI/CD pipelines settings](../user/project/pipelines/settings.md) - [CI/CD pipelines settings](../user/project/pipelines/settings.md)
- [Review Apps](review_apps/index.md) - [Review Apps](review_apps/index.md)
- [Git submodules](git_submodules.md) Using Git submodules in your CI jobs - [Git submodules](git_submodules.md) Using Git submodules in your CI jobs
- [Autodeploy](autodeploy/index.md)
## Breaking changes ## Breaking changes
......
# Autodeploy
> [Introduced][mr-8135] in GitLab 8.15.
Autodeploy is an easy way to configure GitLab CI for the deployment of your
application. GitLab Community maintains a list of `.gitlab-ci.yml`
templates for various infrastructure providers and deployment scripts
powering them. These scripts are responsible for packaging your application,
setting up the infrastructure and spinning up necessary services (for
example a database).
You can use [project services][project-services] to store credentials to
your infrastructure provider and they will be available during the
deployment.
## Supported templates
The list of supported autodeploy templates is available [here][autodeploy-templates].
## Configuration
1. Enable a deployment [project service][project-services] to store your
credentials. For example, if you want to deploy to a Kubernetes cluster
you have to enable [Kubernetes service][kubernetes-service].
1. Configure GitLab Runner to use [docker-in-docker executor][docker-in-docker].
1. Navigate to the "Project" tab and click "Set up autodeploy" button.
![Autodeploy button](img/autodeploy_button.png)
1. Select a template.
![Dropdown with autodeploy templates](img/autodeploy_dropdown.png)
1. Commit your changes and create a merge request.
1. Test your deployment configuration using a [Review App][review-app] that was
created automatically for you.
[mr-8135]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8135
[project-services]: ../../project_services/project_services.md
[autodeploy-templates]: https://gitlab.com/gitlab-org/gitlab-ci-yml/tree/master/autodeploy
[kubernetes-service]: ../../project_services/kubernetes.md
[docker-in-docker]: ../docker/using_docker_build.md#use-docker-in-docker-executor
[review-app]: ../review_apps/index.md
...@@ -9,7 +9,7 @@ See the documentation below for details on how to configure these services. ...@@ -9,7 +9,7 @@ See the documentation below for details on how to configure these services.
- [External issue tracker](external-issue-tracker.md) Redmine, JIRA, etc. - [External issue tracker](external-issue-tracker.md) Redmine, JIRA, etc.
- [LDAP](ldap.md) Set up sign in via LDAP - [LDAP](ldap.md) Set up sign in via LDAP
- [Jenkins](jenkins.md) Integrate with the Jenkins CI - [Jenkins](jenkins.md) Integrate with the Jenkins CI
- [OmniAuth](omniauth.md) Sign in via Twitter, GitHub, GitLab.com, Google, Bitbucket, Facebook, Shibboleth, SAML, Crowd and Azure - [OmniAuth](omniauth.md) Sign in via Twitter, GitHub, GitLab.com, Google, Bitbucket, Facebook, Shibboleth, SAML, Crowd, Azure and Authentiq ID
- [SAML](saml.md) Configure GitLab as a SAML 2.0 Service Provider - [SAML](saml.md) Configure GitLab as a SAML 2.0 Service Provider
- [CAS](cas.md) Configure GitLab to sign in using CAS - [CAS](cas.md) Configure GitLab to sign in using CAS
- [Kerberos](kerberos.md) Integrate with Kerberos - [Kerberos](kerberos.md) Integrate with Kerberos
......
...@@ -40,9 +40,13 @@ you to use. ...@@ -40,9 +40,13 @@ you to use.
| :--- | :---------- | | :--- | :---------- |
| **Name** | This can be anything. Consider something like `<Organization>'s GitLab` or `<Your Name>'s GitLab` or something else descriptive. | | **Name** | This can be anything. Consider something like `<Organization>'s GitLab` or `<Your Name>'s GitLab` or something else descriptive. |
| **Application description** | Fill this in if you wish. | | **Application description** | Fill this in if you wish. |
| **Callback URL** | Leave blank. | | **Callback URL** | The URL to your GitLab installation, e.g., `https://gitlab.example.com`. |
| **URL** | The URL to your GitLab installation, e.g., `https://gitlab.example.com`. | | **URL** | The URL to your GitLab installation, e.g., `https://gitlab.example.com`. |
NOTE: Starting in GitLab 8.15, you MUST specify a callback URL, or you will
see an "Invalid redirect_uri" message. For more details, see [the
Bitbucket documentation](https://confluence.atlassian.com/bitbucket/oauth-faq-338365710.html).
And grant at least the following permissions: And grant at least the following permissions:
``` ```
......
...@@ -30,6 +30,7 @@ contains some settings that are common for all providers. ...@@ -30,6 +30,7 @@ contains some settings that are common for all providers.
- [Crowd](crowd.md) - [Crowd](crowd.md)
- [Azure](azure.md) - [Azure](azure.md)
- [Auth0](auth0.md) - [Auth0](auth0.md)
- [Authentiq](../administration/auth/authentiq.md)
## Initial OmniAuth Configuration ## Initial OmniAuth Configuration
......
...@@ -47,8 +47,6 @@ Feature: Project Commits ...@@ -47,8 +47,6 @@ Feature: Project Commits
And repository contains ".gitlab-ci.yml" file And repository contains ".gitlab-ci.yml" file
When I click on commit link When I click on commit link
Then I see commit ci info Then I see commit ci info
And I click status link
Then I see builds list
Scenario: I browse commit with side-by-side diff view Scenario: I browse commit with side-by-side diff view
Given I click on commit link Given I click on commit link
......
...@@ -166,15 +166,6 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps ...@@ -166,15 +166,6 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps
expect(page).to have_content "Pipeline #1 for 570e7b2a pending" expect(page).to have_content "Pipeline #1 for 570e7b2a pending"
end end
step 'I click status link' do
find('.commit-ci-menu').click_link "Builds"
end
step 'I see builds list' do
expect(page).to have_content "Pipeline #1 for 570e7b2a pending"
expect(page).to have_content "1 build"
end
step 'I search "submodules" commits' do step 'I search "submodules" commits' do
fill_in 'commits-search', with: 'submodules' fill_in 'commits-search', with: 'submodules'
end end
......
...@@ -254,15 +254,26 @@ module Banzai ...@@ -254,15 +254,26 @@ module Banzai
# Returns projects for the given paths. # Returns projects for the given paths.
def find_projects_for_paths(paths) def find_projects_for_paths(paths)
if RequestStore.active? if RequestStore.active?
to_query = paths - project_refs_cache.keys cache = project_refs_cache
to_query = paths - cache.keys
unless to_query.empty? unless to_query.empty?
projects_relation_for_paths(to_query).each do |project| projects = projects_relation_for_paths(to_query)
get_or_set_cache(project_refs_cache, project.path_with_namespace) { project }
found = []
projects.each do |project|
ref = project.path_with_namespace
get_or_set_cache(cache, ref) { project }
found << ref
end
not_found = to_query - found
not_found.each do |ref|
get_or_set_cache(cache, ref) { nil }
end end
end end
project_refs_cache.slice(*paths).values cache.slice(*paths).values.compact
else else
projects_relation_for_paths(paths) projects_relation_for_paths(paths)
end end
......
...@@ -10,7 +10,7 @@ module Banzai ...@@ -10,7 +10,7 @@ module Banzai
node.set_attribute('href', href) node.set_attribute('href', href)
end end
if href =~ /\Ahttp(s)?:\/\// && external_url?(href) if href =~ %r{\A(https?:)?//[^/]} && external_url?(href)
node.set_attribute('rel', 'nofollow noreferrer') node.set_attribute('rel', 'nofollow noreferrer')
node.set_attribute('target', '_blank') node.set_attribute('target', '_blank')
end end
......
...@@ -60,7 +60,7 @@ module Ci ...@@ -60,7 +60,7 @@ module Ci
end end
def build_not_found! def build_not_found!
if headers['User-Agent'].match(/gitlab-ci-multi-runner \d+\.\d+\.\d+(~beta\.\d+\.g[0-9a-f]+)? /) if headers['User-Agent'].to_s.match(/gitlab-ci-multi-runner \d+\.\d+\.\d+(~beta\.\d+\.g[0-9a-f]+)? /)
no_content! no_content!
else else
not_found! not_found!
......
...@@ -70,8 +70,8 @@ module Gitlab ...@@ -70,8 +70,8 @@ module Gitlab
def tag_endpoint(trans, env) def tag_endpoint(trans, env)
endpoint = env[ENDPOINT_KEY] endpoint = env[ENDPOINT_KEY]
path = endpoint_paths_cache[endpoint.route.route_method][endpoint.route.route_path] path = endpoint_paths_cache[endpoint.route.request_method][endpoint.route.path]
trans.action = "Grape##{endpoint.route.route_method} #{path}" trans.action = "Grape##{endpoint.route.request_method} #{path}"
end end
private private
......
...@@ -42,7 +42,7 @@ module Gitlab ...@@ -42,7 +42,7 @@ module Gitlab
key, value = parsed_field.first key, value = parsed_field.first
if value.nil? if value.nil?
value = File.open(tmp_path) value = open_file(tmp_path)
@open_files << value @open_files << value
else else
value = decorate_params_value(value, @request.params[key], tmp_path) value = decorate_params_value(value, @request.params[key], tmp_path)
...@@ -68,7 +68,7 @@ module Gitlab ...@@ -68,7 +68,7 @@ module Gitlab
case path_value case path_value
when nil when nil
value_hash[path_key] = File.open(tmp_path) value_hash[path_key] = open_file(tmp_path)
@open_files << value_hash[path_key] @open_files << value_hash[path_key]
value_hash value_hash
when Hash when Hash
...@@ -78,6 +78,10 @@ module Gitlab ...@@ -78,6 +78,10 @@ module Gitlab
raise "unexpected path value: #{path_value.inspect}" raise "unexpected path value: #{path_value.inspect}"
end end
end end
def open_file(path)
::UploadedFile.new(path, File.basename(path), 'application/octet-stream')
end
end end
def initialize(app) def initialize(app)
......
...@@ -13,8 +13,9 @@ module Gitlab ...@@ -13,8 +13,9 @@ module Gitlab
def categories def categories
{ {
"General" => '', 'General' => '',
"Pages" => 'Pages' 'Pages' => 'Pages',
'Autodeploy' => 'autodeploy'
} }
end end
...@@ -25,6 +26,11 @@ module Gitlab ...@@ -25,6 +26,11 @@ module Gitlab
def finder(project = nil) def finder(project = nil)
Gitlab::Template::Finders::GlobalTemplateFinder.new(self.base_dir, self.extension, self.categories) Gitlab::Template::Finders::GlobalTemplateFinder.new(self.base_dir, self.extension, self.categories)
end end
def dropdown_names(context)
categories = context == 'autodeploy' ? ['Autodeploy'] : ['General', 'Pages']
super().slice(*categories)
end
end end
end end
end end
......
module Gitlab
class UpdatePathError < StandardError; end
end
module Mattermost
class ClientError < Mattermost::Error; end
class Client
attr_reader :user
def initialize(user)
@user = user
end
private
def with_session(&blk)
Mattermost::Session.new(user).with_session(&blk)
end
def json_get(path, options = {})
with_session do |session|
json_response session.get(path, options)
end
end
def json_post(path, options = {})
with_session do |session|
json_response session.post(path, options)
end
end
def json_response(response)
json_response = JSON.parse(response.body)
unless response.success?
raise Mattermost::ClientError.new(json_response['message'] || 'Undefined error')
end
json_response
rescue JSON::JSONError
raise Mattermost::ClientError.new('Cannot parse response')
end
end
end
module Mattermost
class Command < Client
def create(params)
response = json_post("/api/v3/teams/#{params[:team_id]}/commands/create",
body: params.to_json)
response['token']
end
end
end
module Mattermost
class Error < StandardError; end
end
module Mattermost module Mattermost
class NoSessionError < StandardError; end class NoSessionError < Mattermost::Error
def message
'No session could be set up, is Mattermost configured with Single Sign On?'
end
end
class ConnectionError < Mattermost::Error; end
# This class' prime objective is to obtain a session token on a Mattermost # This class' prime objective is to obtain a session token on a Mattermost
# instance with SSO configured where this GitLab instance is the provider. # instance with SSO configured where this GitLab instance is the provider.
# #
...@@ -17,6 +24,8 @@ module Mattermost ...@@ -17,6 +24,8 @@ module Mattermost
include Doorkeeper::Helpers::Controller include Doorkeeper::Helpers::Controller
include HTTParty include HTTParty
LEASE_TIMEOUT = 60
base_uri Settings.mattermost.host base_uri Settings.mattermost.host
attr_accessor :current_resource_owner, :token attr_accessor :current_resource_owner, :token
...@@ -26,14 +35,18 @@ module Mattermost ...@@ -26,14 +35,18 @@ module Mattermost
end end
def with_session def with_session
raise NoSessionError unless create with_lease do
raise Mattermost::NoSessionError unless create
begin begin
yield self yield self
rescue Errno::ECONNREFUSED
raise Mattermost::NoSessionError
ensure ensure
destroy destroy
end end
end end
end
# Next methods are needed for Doorkeeper # Next methods are needed for Doorkeeper
def pre_auth def pre_auth
...@@ -58,12 +71,16 @@ module Mattermost ...@@ -58,12 +71,16 @@ module Mattermost
end end
def get(path, options = {}) def get(path, options = {})
handle_exceptions do
self.class.get(path, options.merge(headers: @headers)) self.class.get(path, options.merge(headers: @headers))
end end
end
def post(path, options = {}) def post(path, options = {})
handle_exceptions do
self.class.post(path, options.merge(headers: @headers)) self.class.post(path, options.merge(headers: @headers))
end end
end
private private
...@@ -111,5 +128,33 @@ module Mattermost ...@@ -111,5 +128,33 @@ module Mattermost
response.headers['token'] response.headers['token']
end end
end end
def with_lease
lease_uuid = lease_try_obtain
raise NoSessionError unless lease_uuid
begin
yield
ensure
Gitlab::ExclusiveLease.cancel(lease_key, lease_uuid)
end
end
def lease_key
"mattermost:session"
end
def lease_try_obtain
lease = ::Gitlab::ExclusiveLease.new(lease_key, timeout: LEASE_TIMEOUT)
lease.try_obtain
end
def handle_exceptions
yield
rescue HTTParty::Error => e
raise Mattermost::ConnectionError.new(e.message)
rescue Errno::ECONNREFUSED
raise Mattermost::ConnectionError.new(e.message)
end
end end
end end
module Mattermost
class Team < Client
def all
json_get('/api/v3/teams/all')
end
end
end
...@@ -105,4 +105,25 @@ describe GroupsController do ...@@ -105,4 +105,25 @@ describe GroupsController do
end end
end end
end end
describe 'PUT update' do
before do
sign_in(user)
end
it 'updates the path succesfully' do
post :update, id: group.to_param, group: { path: 'new_path' }
expect(response).to have_http_status(302)
expect(controller).to set_flash[:notice]
end
it 'does not update the path on error' do
allow_any_instance_of(Group).to receive(:move_dir).and_raise(Gitlab::UpdatePathError)
post :update, id: group.to_param, group: { path: 'new_path' }
expect(assigns(:group).errors).not_to be_empty
expect(assigns(:group).path).not_to eq('new_path')
end
end
end end
require 'spec_helper'
describe Projects::MattermostsController do
let!(:project) { create(:empty_project) }
let!(:user) { create(:user) }
before do
project.team << [user, :master]
sign_in(user)
end
describe 'GET #new' do
before do
allow_any_instance_of(MattermostSlashCommandsService).
to receive(:list_teams).and_return([])
get(:new,
namespace_id: project.namespace.to_param,
project_id: project.to_param)
end
it 'accepts the request' do
expect(response).to have_http_status(200)
end
end
describe 'POST #create' do
let(:mattermost_params) { { trigger: 'http://localhost:3000/trigger', team_id: 'abc' } }
subject do
post(:create,
namespace_id: project.namespace.to_param,
project_id: project.to_param,
mattermost: mattermost_params)
end
context 'no request can be made to mattermost' do
it 'shows the error' do
allow_any_instance_of(MattermostSlashCommandsService).to receive(:configure).and_return([false, "error message"])
expect(subject).to redirect_to(new_namespace_project_mattermost_url(project.namespace, project))
end
end
context 'the request is succesull' do
before do
allow_any_instance_of(Mattermost::Command).to receive(:create).and_return('token')
end
it 'redirects to the new page' do
subject
service = project.services.last
expect(subject).to redirect_to(edit_namespace_project_service_url(project.namespace, project, service))
end
end
end
end
...@@ -780,10 +780,6 @@ describe Projects::MergeRequestsController do ...@@ -780,10 +780,6 @@ describe Projects::MergeRequestsController do
end end
end end
describe 'GET builds' do
it_behaves_like "loads labels", :builds
end
describe 'GET pipelines' do describe 'GET pipelines' do
it_behaves_like "loads labels", :pipelines it_behaves_like "loads labels", :pipelines
end end
......
require 'spec_helper'
describe 'Auto deploy' do
include WaitForAjax
let(:user) { create(:user) }
let(:project) { create(:project) }
before do
project.create_kubernetes_service(
active: true,
properties: {
namespace: project.path,
api_url: 'https://kubernetes.example.com',
token: 'a' * 40,
}
)
project.team << [user, :master]
login_as user
end
context 'when no deployment service is active' do
before do
project.kubernetes_service.update!(active: false)
end
it 'does not show a button to set up auto deploy' do
visit namespace_project_path(project.namespace, project)
expect(page).to have_no_content('Set up autodeploy')
end
end
context 'when a deployment service is active' do
before do
project.kubernetes_service.update!(active: true)
visit namespace_project_path(project.namespace, project)
end
it 'shows a button to set up auto deploy' do
expect(page).to have_link('Set up autodeploy')
end
it 'includes Kubernetes as an available template', js: true do
click_link 'Set up autodeploy'
click_button 'Choose a GitLab CI Yaml template'
within '.gitlab-ci-yml-selector' do
expect(page).to have_content('OpenShift')
end
end
it 'creates a merge request using "autodeploy" branch', js: true do
click_link 'Set up autodeploy'
click_button 'Choose a GitLab CI Yaml template'
within '.gitlab-ci-yml-selector' do
click_on 'OpenShift'
end
wait_for_ajax
click_button 'Commit Changes'
expect(page).to have_content('New Merge Request From autodeploy into master')
end
end
end
...@@ -39,7 +39,6 @@ feature 'GFM autocomplete', feature: true, js: true do ...@@ -39,7 +39,6 @@ feature 'GFM autocomplete', feature: true, js: true do
page.within '.timeline-content-form' do page.within '.timeline-content-form' do
note.native.send_keys('') note.native.send_keys('')
note.native.send_keys("~#{label.title[0]}") note.native.send_keys("~#{label.title[0]}")
sleep 1
note.click note.click
end end
...@@ -53,7 +52,6 @@ feature 'GFM autocomplete', feature: true, js: true do ...@@ -53,7 +52,6 @@ feature 'GFM autocomplete', feature: true, js: true do
page.within '.timeline-content-form' do page.within '.timeline-content-form' do
note.native.send_keys('') note.native.send_keys('')
note.native.send_keys("@#{user.username[0]}") note.native.send_keys("@#{user.username[0]}")
sleep 1
note.click note.click
end end
...@@ -67,7 +65,6 @@ feature 'GFM autocomplete', feature: true, js: true do ...@@ -67,7 +65,6 @@ feature 'GFM autocomplete', feature: true, js: true do
page.within '.timeline-content-form' do page.within '.timeline-content-form' do
note.native.send_keys('') note.native.send_keys('')
note.native.send_keys(":cartwheel") note.native.send_keys(":cartwheel")
sleep 1
note.click note.click
end end
...@@ -76,6 +73,22 @@ feature 'GFM autocomplete', feature: true, js: true do ...@@ -76,6 +73,22 @@ feature 'GFM autocomplete', feature: true, js: true do
expect_to_wrap(false, emoji_item, note, 'cartwheel_tone1') expect_to_wrap(false, emoji_item, note, 'cartwheel_tone1')
end end
it 'doesn\'t open autocomplete after non-word character' do
page.within '.timeline-content-form' do
find('#note_note').native.send_keys("@#{user.username[0..2]}!")
end
expect(page).not_to have_selector('.atwho-view')
end
it 'doesn\'t open autocomplete if there is no space before' do
page.within '.timeline-content-form' do
find('#note_note').native.send_keys("hello:#{user.username[0..2]}")
end
expect(page).not_to have_selector('.atwho-view')
end
def expect_to_wrap(should_wrap, item, note, value) def expect_to_wrap(should_wrap, item, note, value)
expect(item).to have_content(value) expect(item).to have_content(value)
expect(item).not_to have_content("\"#{value}\"") expect(item).not_to have_content("\"#{value}\"")
...@@ -89,12 +102,4 @@ feature 'GFM autocomplete', feature: true, js: true do ...@@ -89,12 +102,4 @@ feature 'GFM autocomplete', feature: true, js: true do
end end
end end
end end
it 'doesnt open autocomplete after non-word character' do
page.within '.timeline-content-form' do
find('#note_note').native.send_keys("@#{user.username[0..2]}!")
end
expect(page).not_to have_selector('.atwho-view')
end
end end
...@@ -54,14 +54,14 @@ feature 'Merge request created from fork' do ...@@ -54,14 +54,14 @@ feature 'Merge request created from fork' do
scenario 'user visits a pipelines page', js: true do scenario 'user visits a pipelines page', js: true do
visit_merge_request(merge_request) visit_merge_request(merge_request)
page.within('.merge-request-tabs') { click_link 'Builds' } page.within('.merge-request-tabs') { click_link 'Pipelines' }
page.within('table.ci-table') do page.within('table.ci-table') do
expect(page).to have_content 'rspec' expect(page).to have_content pipeline.status
expect(page).to have_content 'spinach' expect(page).to have_content pipeline.id
end end
expect(find_link('Cancel running')[:href]) expect(page.find('a.btn-remove')[:href])
.to include fork_project.path_with_namespace .to include fork_project.path_with_namespace
end end
end end
......
require 'spec_helper' require 'spec_helper'
feature 'project commit builds' do feature 'project commit pipelines' do
given(:project) { create(:project) } given(:project) { create(:project) }
background do background do
...@@ -16,11 +16,13 @@ feature 'project commit builds' do ...@@ -16,11 +16,13 @@ feature 'project commit builds' do
ref: 'master') ref: 'master')
end end
scenario 'user views commit builds page' do scenario 'user views commit pipelines page' do
visit builds_namespace_project_commit_path(project.namespace, visit pipelines_namespace_project_commit_path(project.namespace, project, project.commit.sha)
project, project.commit.sha)
expect(page).to have_content('Builds') page.within('.table-holder') do
expect(page).to have_content project.pipelines[0].status # pipeline status
expect(page).to have_content project.pipelines[0].id # pipeline ids
end
end end
end end
end end
...@@ -4,29 +4,26 @@ feature 'Setup Mattermost slash commands', feature: true do ...@@ -4,29 +4,26 @@ feature 'Setup Mattermost slash commands', feature: true do
include WaitForAjax include WaitForAjax
let(:user) { create(:user) } let(:user) { create(:user) }
let(:project) { create(:project) } let(:project) { create(:empty_project) }
let(:service) { project.create_mattermost_slash_commands_service } let(:service) { project.create_mattermost_slash_commands_service }
let(:mattermost_enabled) { true }
before do before do
Settings.mattermost['enabled'] = mattermost_enabled
project.team << [user, :master] project.team << [user, :master]
login_as(user) login_as(user)
visit edit_namespace_project_service_path(project.namespace, project, service)
end end
describe 'user visites the mattermost slash command config page', js: true do describe 'user visits the mattermost slash command config page', js: true do
it 'shows a help message' do it 'shows a help message' do
visit edit_namespace_project_service_path(project.namespace, project, service)
wait_for_ajax wait_for_ajax
expect(page).to have_content("This service allows GitLab users to perform common") expect(page).to have_content("This service allows GitLab users to perform common")
end end
end
describe 'saving a token' do
let(:token) { ('a'..'z').to_a.join }
it 'shows the token after saving' do it 'shows the token after saving' do
visit edit_namespace_project_service_path(project.namespace, project, service) token = ('a'..'z').to_a.join
fill_in 'service_token', with: token fill_in 'service_token', with: token
click_on 'Save' click_on 'Save'
...@@ -35,14 +32,21 @@ feature 'Setup Mattermost slash commands', feature: true do ...@@ -35,14 +32,21 @@ feature 'Setup Mattermost slash commands', feature: true do
expect(value).to eq(token) expect(value).to eq(token)
end end
describe 'mattermost service is enabled' do
it 'shows the add to mattermost button' do
expect(page).to have_link 'Add to Mattermost'
end
end end
describe 'the trigger url' do describe 'mattermost service is not enabled' do
it 'shows the correct url' do let(:mattermost_enabled) { false }
visit edit_namespace_project_service_path(project.namespace, project, service)
it 'shows the correct trigger url' do
value = find_field('request_url').value value = find_field('request_url').value
expect(value).to match("api/v3/projects/#{project.id}/services/mattermost_slash_commands/trigger") expect(value).to match("api/v3/projects/#{project.id}/services/mattermost_slash_commands/trigger")
end end
end end
end
end end
%form.js-filter-form{action: '/user/project/issues?scope=all&state=closed'}
%input{id: 'utf8', name: 'utf8', value: '✓'}
%input{id: 'check_all_issues', name: 'check_all_issues'}
%input{id: 'search', name: 'search'}
%input{id: 'author_id', name: 'author_id'}
%input{id: 'assignee_id', name: 'assignee_id'}
%input{id: 'milestone_title', name: 'milestone_title'}
%input{id: 'label_name', name: 'label_name'}
/* global Issuable */
/* global Turbolinks */
//= require issuable
//= require turbolinks
(() => {
const BASE_URL = '/user/project/issues?scope=all&state=closed';
const DEFAULT_PARAMS = '&utf8=%E2%9C%93';
function updateForm(formValues, form) {
$.each(formValues, (id, value) => {
$(`#${id}`, form).val(value);
});
}
function resetForm(form) {
$('input[name!="utf8"]', form).each((index, input) => {
input.setAttribute('value', '');
});
}
describe('Issuable', () => {
fixture.preload('issuable_filter');
beforeEach(() => {
fixture.load('issuable_filter');
Issuable.init();
});
it('should be defined', () => {
expect(window.Issuable).toBeDefined();
});
describe('filtering', () => {
let $filtersForm;
beforeEach(() => {
$filtersForm = $('.js-filter-form');
fixture.load('issuable_filter');
resetForm($filtersForm);
});
it('should contain only the default parameters', () => {
spyOn(Turbolinks, 'visit');
Issuable.filterResults($filtersForm);
expect(Turbolinks.visit).toHaveBeenCalledWith(BASE_URL + DEFAULT_PARAMS);
});
it('should filter for the phrase "broken"', () => {
spyOn(Turbolinks, 'visit');
updateForm({ search: 'broken' }, $filtersForm);
Issuable.filterResults($filtersForm);
const params = `${DEFAULT_PARAMS}&search=broken`;
expect(Turbolinks.visit).toHaveBeenCalledWith(BASE_URL + params);
});
it('should keep query parameters after modifying filter', () => {
spyOn(Turbolinks, 'visit');
// initial filter
updateForm({ milestone_title: 'v1.0' }, $filtersForm);
Issuable.filterResults($filtersForm);
let params = `${DEFAULT_PARAMS}&milestone_title=v1.0`;
expect(Turbolinks.visit).toHaveBeenCalledWith(BASE_URL + params);
// update filter
updateForm({ label_name: 'Frontend' }, $filtersForm);
Issuable.filterResults($filtersForm);
params = `${DEFAULT_PARAMS}&milestone_title=v1.0&label_name=Frontend`;
expect(Turbolinks.visit).toHaveBeenCalledWith(BASE_URL + params);
});
});
});
})();
...@@ -32,13 +32,64 @@ describe Banzai::Filter::AbstractReferenceFilter do ...@@ -32,13 +32,64 @@ describe Banzai::Filter::AbstractReferenceFilter do
end end
describe '#find_projects_for_paths' do describe '#find_projects_for_paths' do
let(:doc) { Nokogiri::HTML.fragment('') }
let(:filter) { described_class.new(doc, project: project) }
context 'with RequestStore disabled' do
it 'returns a list of Projects for a list of paths' do it 'returns a list of Projects for a list of paths' do
doc = Nokogiri::HTML.fragment('') expect(filter.find_projects_for_paths([project.path_with_namespace])).
filter = described_class.new(doc, project: project) to eq([project])
end
it "return an empty array for paths that don't exist" do
expect(filter.find_projects_for_paths(['nonexistent/project'])).
to eq([])
end
end
context 'with RequestStore enabled' do
before do
RequestStore.begin!
end
after do
RequestStore.end!
RequestStore.clear!
end
it 'returns a list of Projects for a list of paths' do
expect(filter.find_projects_for_paths([project.path_with_namespace])). expect(filter.find_projects_for_paths([project.path_with_namespace])).
to eq([project]) to eq([project])
end end
context "when no project with that path exists" do
it "returns no value" do
expect(filter.find_projects_for_paths(['nonexistent/project'])).
to eq([])
end
it "adds the ref to the project refs cache" do
project_refs_cache = {}
allow(filter).to receive(:project_refs_cache).and_return(project_refs_cache)
filter.find_projects_for_paths(['nonexistent/project'])
expect(project_refs_cache).to eq({ 'nonexistent/project' => nil })
end
context 'when the project refs cache includes nil values' do
before do
# adds { 'nonexistent/project' => nil } to cache
filter.project_from_ref_cached('nonexistent/project')
end
it "return an empty array for paths that don't exist" do
expect(filter.find_projects_for_paths(['nonexistent/project'])).
to eq([])
end
end
end
end
end end
describe '#current_project_path' do describe '#current_project_path' do
......
...@@ -80,4 +80,18 @@ describe Banzai::Filter::ExternalLinkFilter, lib: true do ...@@ -80,4 +80,18 @@ describe Banzai::Filter::ExternalLinkFilter, lib: true do
expect(filter(act).to_html).to eq(exp) expect(filter(act).to_html).to eq(exp)
end end
end end
context 'for protocol-relative links' do
let(:doc) { filter %q(<p><a href="//google.com/">Google</a></p>) }
it 'adds rel="nofollow" to external links' do
expect(doc.at_css('a')).to have_attribute('rel')
expect(doc.at_css('a')['rel']).to include 'nofollow'
end
it 'adds rel="noreferrer" to external links' do
expect(doc.at_css('a')).to have_attribute('rel')
expect(doc.at_css('a')['rel']).to include 'noreferrer'
end
end
end end
...@@ -33,7 +33,7 @@ describe Gitlab::Metrics::RackMiddleware do ...@@ -33,7 +33,7 @@ describe Gitlab::Metrics::RackMiddleware do
end end
it 'tags a transaction with the method and path of the route in the grape endpoint' do it 'tags a transaction with the method and path of the route in the grape endpoint' do
route = double(:route, route_method: "GET", route_path: "/:version/projects/:id/archive(.:format)") route = double(:route, request_method: "GET", path: "/:version/projects/:id/archive(.:format)")
endpoint = double(:endpoint, route: route) endpoint = double(:endpoint, route: route)
env['api.endpoint'] = endpoint env['api.endpoint'] = endpoint
...@@ -117,7 +117,7 @@ describe Gitlab::Metrics::RackMiddleware do ...@@ -117,7 +117,7 @@ describe Gitlab::Metrics::RackMiddleware do
let(:transaction) { middleware.transaction_from_env(env) } let(:transaction) { middleware.transaction_from_env(env) }
it 'tags a transaction with the method and path of the route in the grape endpount' do it 'tags a transaction with the method and path of the route in the grape endpount' do
route = double(:route, route_method: "GET", route_path: "/:version/projects/:id/archive(.:format)") route = double(:route, request_method: "GET", path: "/:version/projects/:id/archive(.:format)")
endpoint = double(:endpoint, route: route) endpoint = double(:endpoint, route: route)
env['api.endpoint'] = endpoint env['api.endpoint'] = endpoint
......
...@@ -12,7 +12,7 @@ describe Gitlab::Middleware::Multipart do ...@@ -12,7 +12,7 @@ describe Gitlab::Middleware::Multipart do
expect(app).to receive(:call) do |env| expect(app).to receive(:call) do |env|
file = Rack::Request.new(env).params['file'] file = Rack::Request.new(env).params['file']
expect(file).to be_a(File) expect(file).to be_a(::UploadedFile)
expect(file.path).to eq(tempfile.path) expect(file.path).to eq(tempfile.path)
end end
...@@ -39,7 +39,7 @@ describe Gitlab::Middleware::Multipart do ...@@ -39,7 +39,7 @@ describe Gitlab::Middleware::Multipart do
expect(app).to receive(:call) do |env| expect(app).to receive(:call) do |env|
file = Rack::Request.new(env).params['user']['avatar'] file = Rack::Request.new(env).params['user']['avatar']
expect(file).to be_a(File) expect(file).to be_a(::UploadedFile)
expect(file.path).to eq(tempfile.path) expect(file.path).to eq(tempfile.path)
end end
...@@ -54,7 +54,7 @@ describe Gitlab::Middleware::Multipart do ...@@ -54,7 +54,7 @@ describe Gitlab::Middleware::Multipart do
expect(app).to receive(:call) do |env| expect(app).to receive(:call) do |env|
file = Rack::Request.new(env).params['project']['milestone']['themesong'] file = Rack::Request.new(env).params['project']['milestone']['themesong']
expect(file).to be_a(File) expect(file).to be_a(::UploadedFile)
expect(file.path).to eq(tempfile.path) expect(file.path).to eq(tempfile.path)
end end
......
require 'spec_helper'
describe Mattermost::Client do
let(:user) { build(:user) }
subject { described_class.new(user) }
context 'JSON parse error' do
before do
Struct.new("Request", :body, :success?)
end
it 'yields an error on malformed JSON' do
bad_json = Struct::Request.new("I'm not json", true)
expect { subject.send(:json_response, bad_json) }.to raise_error(Mattermost::ClientError)
end
it 'shows a client error if the request was unsuccessful' do
bad_request = Struct::Request.new("true", false)
expect { subject.send(:json_response, bad_request) }.to raise_error(Mattermost::ClientError)
end
end
end
require 'spec_helper'
describe Mattermost::Command do
let(:params) { { 'token' => 'token', team_id: 'abc' } }
before do
Mattermost::Session.base_uri('http://mattermost.example.com')
allow_any_instance_of(Mattermost::Client).to receive(:with_session).
and_yield(Mattermost::Session.new(nil))
end
describe '#create' do
let(:params) do
{ team_id: 'abc',
trigger: 'gitlab'
}
end
subject { described_class.new(nil).create(params) }
context 'for valid trigger word' do
before do
stub_request(:post, 'http://mattermost.example.com/api/v3/teams/abc/commands/create').
with(body: {
team_id: 'abc',
trigger: 'gitlab' }.to_json).
to_return(
status: 200,
headers: { 'Content-Type' => 'application/json' },
body: { token: 'token' }.to_json
)
end
it 'returns a token' do
is_expected.to eq('token')
end
end
context 'for error message' do
before do
stub_request(:post, 'http://mattermost.example.com/api/v3/teams/abc/commands/create').
to_return(
status: 500,
headers: { 'Content-Type' => 'application/json' },
body: {
id: 'api.command.duplicate_trigger.app_error',
message: 'This trigger word is already in use. Please choose another word.',
detailed_error: '',
request_id: 'obc374man7bx5r3dbc1q5qhf3r',
status_code: 500
}.to_json
)
end
it 'raises an error with message' do
expect { subject }.to raise_error(Mattermost::Error, 'This trigger word is already in use. Please choose another word.')
end
end
end
end
...@@ -95,5 +95,29 @@ describe Mattermost::Session, type: :request do ...@@ -95,5 +95,29 @@ describe Mattermost::Session, type: :request do
end end
end end
end end
context 'with lease' do
before do
allow(subject).to receive(:lease_try_obtain).and_return('aldkfjsldfk')
end
it 'tries to obtain a lease' do
expect(subject).to receive(:lease_try_obtain)
expect(Gitlab::ExclusiveLease).to receive(:cancel)
# Cannot setup a session, but we should still cancel the lease
expect { subject.with_session }.to raise_error(Mattermost::NoSessionError)
end
end
context 'without lease' do
before do
allow(subject).to receive(:lease_try_obtain).and_return(nil)
end
it 'returns a NoSessionError error' do
expect { subject.with_session }.to raise_error(Mattermost::NoSessionError)
end
end
end end
end end
require 'spec_helper'
describe Mattermost::Team do
before do
Mattermost::Session.base_uri('http://mattermost.example.com')
allow_any_instance_of(Mattermost::Client).to receive(:with_session).
and_yield(Mattermost::Session.new(nil))
end
describe '#all' do
subject { described_class.new(nil).all }
context 'for valid request' do
let(:response) do
[{
"id" => "xiyro8huptfhdndadpz8r3wnbo",
"create_at" => 1482174222155,
"update_at" => 1482174222155,
"delete_at" => 0,
"display_name" => "chatops",
"name" => "chatops",
"email" => "admin@example.com",
"type" => "O",
"company_name" => "",
"allowed_domains" => "",
"invite_id" => "o4utakb9jtb7imctdfzbf9r5ro",
"allow_open_invite" => false }]
end
before do
stub_request(:get, 'http://mattermost.example.com/api/v3/teams/all').
to_return(
status: 200,
headers: { 'Content-Type' => 'application/json' },
body: response.to_json
)
end
it 'returns a token' do
is_expected.to eq(response)
end
end
context 'for error message' do
before do
stub_request(:get, 'http://mattermost.example.com/api/v3/teams/all').
to_return(
status: 500,
headers: { 'Content-Type' => 'application/json' },
body: {
id: 'api.team.list.app_error',
message: 'Cannot list teams.',
detailed_error: '',
request_id: 'obc374man7bx5r3dbc1q5qhf3r',
status_code: 500
}.to_json
)
end
it 'raises an error with message' do
expect { subject }.to raise_error(Mattermost::Error, 'Cannot list teams.')
end
end
end
end
...@@ -2,4 +2,123 @@ require 'spec_helper' ...@@ -2,4 +2,123 @@ require 'spec_helper'
describe MattermostSlashCommandsService, :models do describe MattermostSlashCommandsService, :models do
it_behaves_like "chat slash commands service" it_behaves_like "chat slash commands service"
context 'Mattermost API' do
let(:project) { create(:empty_project) }
let(:service) { project.build_mattermost_slash_commands_service }
let(:user) { create(:user)}
before do
Mattermost::Session.base_uri("http://mattermost.example.com")
allow_any_instance_of(Mattermost::Client).to receive(:with_session).
and_yield(Mattermost::Session.new(nil))
end
describe '#configure' do
subject do
service.configure(user, team_id: 'abc',
trigger: 'gitlab', url: 'http://trigger.url',
icon_url: 'http://icon.url/icon.png')
end
context 'the requests succeeds' do
before do
stub_request(:post, 'http://mattermost.example.com/api/v3/teams/abc/commands/create').
with(body: {
team_id: 'abc',
trigger: 'gitlab',
url: 'http://trigger.url',
icon_url: 'http://icon.url/icon.png',
auto_complete: true,
auto_complete_desc: "Perform common operations on: #{project.name_with_namespace}",
auto_complete_hint: '[help]',
description: "Perform common operations on: #{project.name_with_namespace}",
display_name: "GitLab / #{project.name_with_namespace}",
method: 'P',
user_name: 'GitLab' }.to_json).
to_return(
status: 200,
headers: { 'Content-Type' => 'application/json' },
body: { token: 'token' }.to_json
)
end
it 'saves the service' do
expect { subject }.to change { project.services.count }.by(1)
end
it 'saves the token' do
subject
expect(service.reload.token).to eq('token')
end
end
context 'an error is received' do
before do
stub_request(:post, 'http://mattermost.example.com/api/v3/teams/abc/commands/create').
to_return(
status: 500,
headers: { 'Content-Type' => 'application/json' },
body: {
id: 'api.command.duplicate_trigger.app_error',
message: 'This trigger word is already in use. Please choose another word.',
detailed_error: '',
request_id: 'obc374man7bx5r3dbc1q5qhf3r',
status_code: 500
}.to_json
)
end
it 'shows error messages' do
succeeded, message = subject
expect(succeeded).to be(false)
expect(message).to eq('This trigger word is already in use. Please choose another word.')
end
end
end
describe '#list_teams' do
subject do
service.list_teams(user)
end
context 'the requests succeeds' do
before do
stub_request(:get, 'http://mattermost.example.com/api/v3/teams/all').
to_return(
status: 200,
headers: { 'Content-Type' => 'application/json' },
body: ['list'].to_json
)
end
it 'returns a list of teams' do
expect(subject).not_to be_empty
end
end
context 'an error is received' do
before do
stub_request(:get, 'http://mattermost.example.com/api/v3/teams/all').
to_return(
status: 500,
headers: { 'Content-Type' => 'application/json' },
body: {
message: 'Failed to get team list.'
}.to_json
)
end
it 'shows error messages' do
teams, message = subject
expect(teams).to be_empty
expect(message).to eq('Failed to get team list.')
end
end
end
end
end end
...@@ -37,6 +37,11 @@ describe Ci::API::Builds do ...@@ -37,6 +37,11 @@ describe Ci::API::Builds do
let(:user_agent) { 'Go-http-client/1.1' } let(:user_agent) { 'Go-http-client/1.1' }
it { expect(response).to have_http_status(404) } it { expect(response).to have_http_status(404) }
end end
context "when runner doesn't have a User-Agent" do
let(:user_agent) { nil }
it { expect(response).to have_http_status(404) }
end
end end
context 'when there is a pending build' do context 'when there is a pending build' do
......
...@@ -9,7 +9,7 @@ describe Groups::UpdateService, services: true do ...@@ -9,7 +9,7 @@ describe Groups::UpdateService, services: true do
describe "#execute" do describe "#execute" do
context "project visibility_level validation" do context "project visibility_level validation" do
context "public group with public projects" do context "public group with public projects" do
let!(:service) { described_class.new(public_group, user, visibility_level: Gitlab::VisibilityLevel::INTERNAL ) } let!(:service) { described_class.new(public_group, user, visibility_level: Gitlab::VisibilityLevel::INTERNAL) }
before do before do
public_group.add_user(user, Gitlab::Access::MASTER) public_group.add_user(user, Gitlab::Access::MASTER)
...@@ -23,7 +23,7 @@ describe Groups::UpdateService, services: true do ...@@ -23,7 +23,7 @@ describe Groups::UpdateService, services: true do
end end
context "internal group with internal project" do context "internal group with internal project" do
let!(:service) { described_class.new(internal_group, user, visibility_level: Gitlab::VisibilityLevel::PRIVATE ) } let!(:service) { described_class.new(internal_group, user, visibility_level: Gitlab::VisibilityLevel::PRIVATE) }
before do before do
internal_group.add_user(user, Gitlab::Access::MASTER) internal_group.add_user(user, Gitlab::Access::MASTER)
...@@ -39,7 +39,7 @@ describe Groups::UpdateService, services: true do ...@@ -39,7 +39,7 @@ describe Groups::UpdateService, services: true do
end end
context "unauthorized visibility_level validation" do context "unauthorized visibility_level validation" do
let!(:service) { described_class.new(internal_group, user, visibility_level: 99 ) } let!(:service) { described_class.new(internal_group, user, visibility_level: 99) }
before do before do
internal_group.add_user(user, Gitlab::Access::MASTER) internal_group.add_user(user, Gitlab::Access::MASTER)
end end
...@@ -49,4 +49,41 @@ describe Groups::UpdateService, services: true do ...@@ -49,4 +49,41 @@ describe Groups::UpdateService, services: true do
expect(internal_group.errors.count).to eq(1) expect(internal_group.errors.count).to eq(1)
end end
end end
context 'rename group' do
let!(:service) { described_class.new(internal_group, user, path: 'new_path') }
before do
internal_group.add_user(user, Gitlab::Access::MASTER)
create(:project, :internal, group: internal_group)
end
it 'returns true' do
expect(service.execute).to eq(true)
end
context 'error moving group' do
before do
allow(internal_group).to receive(:move_dir).and_raise(Gitlab::UpdatePathError)
end
it 'does not raise an error' do
expect { service.execute }.not_to raise_error
end
it 'returns false' do
expect(service.execute).to eq(false)
end
it 'has the right error' do
service.execute
expect(internal_group.errors.full_messages.first).to eq('Gitlab::UpdatePathError')
end
it "hasn't changed the path" do
expect { service.execute}.not_to change { internal_group.reload.path}
end
end
end
end end
...@@ -35,8 +35,10 @@ ...@@ -35,8 +35,10 @@
1. Clone KaTeX. Anything later than 4fb9445a9 (is merged into master) will do. 1. Clone KaTeX. Anything later than 4fb9445a9 (is merged into master) will do.
2. make (requires node) 2. make (requires node)
3. sed -i 's,fonts/,,' build/katex.css 3. sed -e 's,fonts/,,' -e 's/url\(([^)]*)\)/url(font-path\1)/g' build/katex.css > build/katex.scss
4. Copy build/katex.js, build/katex.css and fonts/* to gitlab. 4. Copy build/katex.js to gitlab/vendor/assets/javascripts/katex.js,
build/katex.scss to gitlab/vendor/assets/stylesheets/katex.scss and
fonts/* to gitlab/vendor/assets/fonts/.
*/ */
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.katex = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){ (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.katex = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
......
...@@ -35,119 +35,121 @@ SOFTWARE. ...@@ -35,119 +35,121 @@ SOFTWARE.
1. Clone KaTeX. Anything later than 4fb9445a9 (is merged into master) will do. 1. Clone KaTeX. Anything later than 4fb9445a9 (is merged into master) will do.
2. make (requires node) 2. make (requires node)
3. sed -i 's,fonts/,,' build/katex.css 3. sed -e 's,fonts/,,' -e 's/url\(([^)]*)\)/url(font-path\1)/g' build/katex.css > build/katex.scss
4. Copy build/katex.js, build/katex.css and fonts/* to gitlab. 4. Copy build/katex.js to gitlab/vendor/assets/javascripts/katex.js,
build/katex.scss to gitlab/vendor/assets/stylesheets/katex.scss and
fonts/* to gitlab/vendor/assets/fonts/.
*/ */
@font-face { @font-face {
font-family: 'KaTeX_AMS'; font-family: 'KaTeX_AMS';
src: url('KaTeX_AMS-Regular.eot'); src: url(font-path('KaTeX_AMS-Regular.eot'));
src: url('KaTeX_AMS-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_AMS-Regular.woff2') format('woff2'), url('KaTeX_AMS-Regular.woff') format('woff'), url('KaTeX_AMS-Regular.ttf') format('truetype'); src: url(font-path('KaTeX_AMS-Regular.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_AMS-Regular.woff2')) format('woff2'), url(font-path('KaTeX_AMS-Regular.woff')) format('woff'), url(font-path('KaTeX_AMS-Regular.ttf')) format('truetype');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
} }
@font-face { @font-face {
font-family: 'KaTeX_Caligraphic'; font-family: 'KaTeX_Caligraphic';
src: url('KaTeX_Caligraphic-Bold.eot'); src: url(font-path('KaTeX_Caligraphic-Bold.eot'));
src: url('KaTeX_Caligraphic-Bold.eot#iefix') format('embedded-opentype'), url('KaTeX_Caligraphic-Bold.woff2') format('woff2'), url('KaTeX_Caligraphic-Bold.woff') format('woff'), url('KaTeX_Caligraphic-Bold.ttf') format('truetype'); src: url(font-path('KaTeX_Caligraphic-Bold.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Caligraphic-Bold.woff2')) format('woff2'), url(font-path('KaTeX_Caligraphic-Bold.woff')) format('woff'), url(font-path('KaTeX_Caligraphic-Bold.ttf')) format('truetype');
font-weight: bold; font-weight: bold;
font-style: normal; font-style: normal;
} }
@font-face { @font-face {
font-family: 'KaTeX_Caligraphic'; font-family: 'KaTeX_Caligraphic';
src: url('KaTeX_Caligraphic-Regular.eot'); src: url(font-path('KaTeX_Caligraphic-Regular.eot'));
src: url('KaTeX_Caligraphic-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_Caligraphic-Regular.woff2') format('woff2'), url('KaTeX_Caligraphic-Regular.woff') format('woff'), url('KaTeX_Caligraphic-Regular.ttf') format('truetype'); src: url(font-path('KaTeX_Caligraphic-Regular.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Caligraphic-Regular.woff2')) format('woff2'), url(font-path('KaTeX_Caligraphic-Regular.woff')) format('woff'), url(font-path('KaTeX_Caligraphic-Regular.ttf')) format('truetype');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
} }
@font-face { @font-face {
font-family: 'KaTeX_Fraktur'; font-family: 'KaTeX_Fraktur';
src: url('KaTeX_Fraktur-Bold.eot'); src: url(font-path('KaTeX_Fraktur-Bold.eot'));
src: url('KaTeX_Fraktur-Bold.eot#iefix') format('embedded-opentype'), url('KaTeX_Fraktur-Bold.woff2') format('woff2'), url('KaTeX_Fraktur-Bold.woff') format('woff'), url('KaTeX_Fraktur-Bold.ttf') format('truetype'); src: url(font-path('KaTeX_Fraktur-Bold.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Fraktur-Bold.woff2')) format('woff2'), url(font-path('KaTeX_Fraktur-Bold.woff')) format('woff'), url(font-path('KaTeX_Fraktur-Bold.ttf')) format('truetype');
font-weight: bold; font-weight: bold;
font-style: normal; font-style: normal;
} }
@font-face { @font-face {
font-family: 'KaTeX_Fraktur'; font-family: 'KaTeX_Fraktur';
src: url('KaTeX_Fraktur-Regular.eot'); src: url(font-path('KaTeX_Fraktur-Regular.eot'));
src: url('KaTeX_Fraktur-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_Fraktur-Regular.woff2') format('woff2'), url('KaTeX_Fraktur-Regular.woff') format('woff'), url('KaTeX_Fraktur-Regular.ttf') format('truetype'); src: url(font-path('KaTeX_Fraktur-Regular.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Fraktur-Regular.woff2')) format('woff2'), url(font-path('KaTeX_Fraktur-Regular.woff')) format('woff'), url(font-path('KaTeX_Fraktur-Regular.ttf')) format('truetype');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
} }
@font-face { @font-face {
font-family: 'KaTeX_Main'; font-family: 'KaTeX_Main';
src: url('KaTeX_Main-Bold.eot'); src: url(font-path('KaTeX_Main-Bold.eot'));
src: url('KaTeX_Main-Bold.eot#iefix') format('embedded-opentype'), url('KaTeX_Main-Bold.woff2') format('woff2'), url('KaTeX_Main-Bold.woff') format('woff'), url('KaTeX_Main-Bold.ttf') format('truetype'); src: url(font-path('KaTeX_Main-Bold.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Main-Bold.woff2')) format('woff2'), url(font-path('KaTeX_Main-Bold.woff')) format('woff'), url(font-path('KaTeX_Main-Bold.ttf')) format('truetype');
font-weight: bold; font-weight: bold;
font-style: normal; font-style: normal;
} }
@font-face { @font-face {
font-family: 'KaTeX_Main'; font-family: 'KaTeX_Main';
src: url('KaTeX_Main-Italic.eot'); src: url(font-path('KaTeX_Main-Italic.eot'));
src: url('KaTeX_Main-Italic.eot#iefix') format('embedded-opentype'), url('KaTeX_Main-Italic.woff2') format('woff2'), url('KaTeX_Main-Italic.woff') format('woff'), url('KaTeX_Main-Italic.ttf') format('truetype'); src: url(font-path('KaTeX_Main-Italic.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Main-Italic.woff2')) format('woff2'), url(font-path('KaTeX_Main-Italic.woff')) format('woff'), url(font-path('KaTeX_Main-Italic.ttf')) format('truetype');
font-weight: normal; font-weight: normal;
font-style: italic; font-style: italic;
} }
@font-face { @font-face {
font-family: 'KaTeX_Main'; font-family: 'KaTeX_Main';
src: url('KaTeX_Main-Regular.eot'); src: url(font-path('KaTeX_Main-Regular.eot'));
src: url('KaTeX_Main-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_Main-Regular.woff2') format('woff2'), url('KaTeX_Main-Regular.woff') format('woff'), url('KaTeX_Main-Regular.ttf') format('truetype'); src: url(font-path('KaTeX_Main-Regular.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Main-Regular.woff2')) format('woff2'), url(font-path('KaTeX_Main-Regular.woff')) format('woff'), url(font-path('KaTeX_Main-Regular.ttf')) format('truetype');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
} }
@font-face { @font-face {
font-family: 'KaTeX_Math'; font-family: 'KaTeX_Math';
src: url('KaTeX_Math-Italic.eot'); src: url(font-path('KaTeX_Math-Italic.eot'));
src: url('KaTeX_Math-Italic.eot#iefix') format('embedded-opentype'), url('KaTeX_Math-Italic.woff2') format('woff2'), url('KaTeX_Math-Italic.woff') format('woff'), url('KaTeX_Math-Italic.ttf') format('truetype'); src: url(font-path('KaTeX_Math-Italic.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Math-Italic.woff2')) format('woff2'), url(font-path('KaTeX_Math-Italic.woff')) format('woff'), url(font-path('KaTeX_Math-Italic.ttf')) format('truetype');
font-weight: normal; font-weight: normal;
font-style: italic; font-style: italic;
} }
@font-face { @font-face {
font-family: 'KaTeX_SansSerif'; font-family: 'KaTeX_SansSerif';
src: url('KaTeX_SansSerif-Regular.eot'); src: url(font-path('KaTeX_SansSerif-Regular.eot'));
src: url('KaTeX_SansSerif-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_SansSerif-Regular.woff2') format('woff2'), url('KaTeX_SansSerif-Regular.woff') format('woff'), url('KaTeX_SansSerif-Regular.ttf') format('truetype'); src: url(font-path('KaTeX_SansSerif-Regular.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_SansSerif-Regular.woff2')) format('woff2'), url(font-path('KaTeX_SansSerif-Regular.woff')) format('woff'), url(font-path('KaTeX_SansSerif-Regular.ttf')) format('truetype');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
} }
@font-face { @font-face {
font-family: 'KaTeX_Script'; font-family: 'KaTeX_Script';
src: url('KaTeX_Script-Regular.eot'); src: url(font-path('KaTeX_Script-Regular.eot'));
src: url('KaTeX_Script-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_Script-Regular.woff2') format('woff2'), url('KaTeX_Script-Regular.woff') format('woff'), url('KaTeX_Script-Regular.ttf') format('truetype'); src: url(font-path('KaTeX_Script-Regular.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Script-Regular.woff2')) format('woff2'), url(font-path('KaTeX_Script-Regular.woff')) format('woff'), url(font-path('KaTeX_Script-Regular.ttf')) format('truetype');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
} }
@font-face { @font-face {
font-family: 'KaTeX_Size1'; font-family: 'KaTeX_Size1';
src: url('KaTeX_Size1-Regular.eot'); src: url(font-path('KaTeX_Size1-Regular.eot'));
src: url('KaTeX_Size1-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_Size1-Regular.woff2') format('woff2'), url('KaTeX_Size1-Regular.woff') format('woff'), url('KaTeX_Size1-Regular.ttf') format('truetype'); src: url(font-path('KaTeX_Size1-Regular.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Size1-Regular.woff2')) format('woff2'), url(font-path('KaTeX_Size1-Regular.woff')) format('woff'), url(font-path('KaTeX_Size1-Regular.ttf')) format('truetype');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
} }
@font-face { @font-face {
font-family: 'KaTeX_Size2'; font-family: 'KaTeX_Size2';
src: url('KaTeX_Size2-Regular.eot'); src: url(font-path('KaTeX_Size2-Regular.eot'));
src: url('KaTeX_Size2-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_Size2-Regular.woff2') format('woff2'), url('KaTeX_Size2-Regular.woff') format('woff'), url('KaTeX_Size2-Regular.ttf') format('truetype'); src: url(font-path('KaTeX_Size2-Regular.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Size2-Regular.woff2')) format('woff2'), url(font-path('KaTeX_Size2-Regular.woff')) format('woff'), url(font-path('KaTeX_Size2-Regular.ttf')) format('truetype');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
} }
@font-face { @font-face {
font-family: 'KaTeX_Size3'; font-family: 'KaTeX_Size3';
src: url('KaTeX_Size3-Regular.eot'); src: url(font-path('KaTeX_Size3-Regular.eot'));
src: url('KaTeX_Size3-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_Size3-Regular.woff2') format('woff2'), url('KaTeX_Size3-Regular.woff') format('woff'), url('KaTeX_Size3-Regular.ttf') format('truetype'); src: url(font-path('KaTeX_Size3-Regular.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Size3-Regular.woff2')) format('woff2'), url(font-path('KaTeX_Size3-Regular.woff')) format('woff'), url(font-path('KaTeX_Size3-Regular.ttf')) format('truetype');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
} }
@font-face { @font-face {
font-family: 'KaTeX_Size4'; font-family: 'KaTeX_Size4';
src: url('KaTeX_Size4-Regular.eot'); src: url(font-path('KaTeX_Size4-Regular.eot'));
src: url('KaTeX_Size4-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_Size4-Regular.woff2') format('woff2'), url('KaTeX_Size4-Regular.woff') format('woff'), url('KaTeX_Size4-Regular.ttf') format('truetype'); src: url(font-path('KaTeX_Size4-Regular.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Size4-Regular.woff2')) format('woff2'), url(font-path('KaTeX_Size4-Regular.woff')) format('woff'), url(font-path('KaTeX_Size4-Regular.ttf')) format('truetype');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
} }
@font-face { @font-face {
font-family: 'KaTeX_Typewriter'; font-family: 'KaTeX_Typewriter';
src: url('KaTeX_Typewriter-Regular.eot'); src: url(font-path('KaTeX_Typewriter-Regular.eot'));
src: url('KaTeX_Typewriter-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_Typewriter-Regular.woff2') format('woff2'), url('KaTeX_Typewriter-Regular.woff') format('woff'), url('KaTeX_Typewriter-Regular.ttf') format('truetype'); src: url(font-path('KaTeX_Typewriter-Regular.eot#iefix')) format('embedded-opentype'), url(font-path('KaTeX_Typewriter-Regular.woff2')) format('woff2'), url(font-path('KaTeX_Typewriter-Regular.woff')) format('woff'), url(font-path('KaTeX_Typewriter-Regular.ttf')) format('truetype');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
} }
......
image: registry.gitlab.com/gitlab-examples/openshift-deploy
variables:
# Application deployment domain
KUBE_DOMAIN: domain.example.com
stages:
- build
- test
- review
- staging
- production
build:
stage: build
script:
- command build
only:
- branches
production:
stage: production
variables:
CI_ENVIRONMENT_URL: http://production.$KUBE_DOMAIN
script:
- command deploy
environment:
name: production
url: http://production.$KUBE_DOMAIN
when: manual
only:
- master
staging:
stage: staging
variables:
CI_ENVIRONMENT_URL: http://staging.$KUBE_DOMAIN
script:
- command deploy
environment:
name: staging
url: http://staging.$KUBE_DOMAIN
only:
- master
review:
stage: review
variables:
CI_ENVIRONMENT_URL: http://$CI_ENVIRONMENT_SLUG.$KUBE_DOMAIN
script:
- command deploy
environment:
name: review/$CI_BUILD_REF_NAME
url: http://$CI_ENVIRONMENT_SLUG.$KUBE_DOMAIN
on_stop: stop_review
only:
- branches
except:
- master
stop_review:
stage: review
variables:
GIT_STRATEGY: none
script:
- command destroy
environment:
name: review/$CI_BUILD_REF_NAME
action: stop
when: manual
only:
- branches
except:
- master
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