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'
gem 'omniauth-twitter', '~> 1.2.0'
gem 'omniauth_crowd', '~> 2.2.0'
gem 'gssapi', group: :kerberos
gem 'omniauth-authentiq', '~> 0.2.0'
gem 'rack-oauth2', '~> 1.2.1'
gem 'jwt'
......
......@@ -452,6 +452,8 @@ GEM
rack (>= 1.0, < 3)
omniauth-auth0 (1.4.1)
omniauth-oauth2 (~> 1.1)
omniauth-authentiq (0.2.2)
omniauth-oauth2 (~> 1.3, >= 1.3.1)
omniauth-azure-oauth2 (0.0.6)
jwt (~> 1.0)
omniauth (~> 1.0)
......@@ -930,6 +932,7 @@ DEPENDENCIES
oj (~> 2.17.4)
omniauth (~> 1.3.1)
omniauth-auth0 (~> 1.4.1)
omniauth-authentiq (~> 0.2.0)
omniauth-azure-oauth2 (~> 0.0.6)
omniauth-cas3 (~> 1.1.2)
omniauth-facebook (~> 4.0.0)
......
......@@ -4,6 +4,7 @@
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
var AUTO_SCROLL_OFFSET = 75;
this.Build = (function() {
Build.interval = null;
......@@ -19,6 +20,17 @@
this.buildStage = options.buildStage;
this.updateDropdown = bind(this.updateDropdown, this);
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);
// Init breakpoint checker
this.bp = Breakpoints.get();
......@@ -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', '.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));
$('a', this.$buildScroll).off('click.stepTrace').on('click.stepTrace', this.stepTrace);
this.updateArtifactRemoveDate();
......@@ -40,18 +53,6 @@
this.initScrollButtonAffix();
}
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) {
// Check for new build output if user still watching build page
// Only valid for runnig build when output changes during time
......@@ -91,9 +92,10 @@
success: function(buildData) {
$('.js-build-output').html(buildData.trace_html);
if (removeRefreshStatuses.indexOf(buildData.status) >= 0) {
return $('.js-build-refresh').remove();
}
this.initScrollMonitor();
return this.$buildRefreshAnimation.remove();
}
}.bind(this)
});
};
......@@ -122,22 +124,95 @@
};
Build.prototype.checkAutoscroll = function() {
if ("enabled" === $("#autoscroll-button").data("state")) {
return $("html,body").scrollTop($("#build-trace").height());
if (this.$autoScrollStatus.data("state") === "enabled") {
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() {
var $body, $buildTrace;
$body = $('body');
$buildTrace = $('#build-trace');
return this.$buildScroll.affix({
offset: {
bottom: function() {
return $body.outerHeight() - ($buildTrace.outerHeight() + $buildTrace.offset().top);
// Hide everything initially
this.$scrollTopBtn.hide();
this.$scrollBottomBtn.hide();
this.$autoScrollContainer.hide();
}
// 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() {
......
......@@ -141,7 +141,6 @@
new MergedButtons();
break;
case 'projects:merge_requests:commits':
case 'projects:merge_requests:builds':
new MergedButtons();
break;
case 'projects:merge_requests:pipelines':
......@@ -171,9 +170,6 @@
container: '.js-pipeline-table',
});
break;
case 'projects:commit:builds':
new gl.Pipelines();
break;
case 'projects:commits:show':
case 'projects:activity':
shortcut_handler = new ShortcutsNavigation();
......
......@@ -112,7 +112,6 @@
return value.path != null ? this.Emoji.template : this.Loading.template;
}.bind(this),
insertTpl: ':${name}:',
startWithSpace: false,
skipSpecialCharacterTest: true,
data: this.defaultLoadingData,
callbacks: {
......@@ -129,7 +128,6 @@
}.bind(this),
insertTpl: '${atwho-at}${username}',
searchKey: 'search',
startWithSpace: false,
alwaysHighlightFirst: true,
skipSpecialCharacterTest: true,
data: this.defaultLoadingData,
......@@ -172,7 +170,6 @@
}.bind(this),
data: this.defaultLoadingData,
insertTpl: '${atwho-at}${id}',
startWithSpace: false,
callbacks: {
sorter: this.DefaultOptions.sorter,
filter: this.DefaultOptions.filter,
......@@ -200,7 +197,6 @@
displayTpl: function(value) {
return value.title != null ? this.Milestones.template : this.Loading.template;
}.bind(this),
startWithSpace: false,
data: this.defaultLoadingData,
callbacks: {
matcher: this.DefaultOptions.matcher,
......@@ -225,7 +221,6 @@
at: '!',
alias: 'mergerequests',
searchKey: 'search',
startWithSpace: false,
displayTpl: function(value) {
return value.title != null ? this.Issues.template : this.Loading.template;
}.bind(this),
......@@ -259,7 +254,6 @@
return this.isLoading(value) ? this.Loading.template : this.Labels.template;
}.bind(this),
insertTpl: '${atwho-at}${title}',
startWithSpace: false,
callbacks: {
matcher: this.DefaultOptions.matcher,
sorter: this.DefaultOptions.sorter,
......@@ -379,14 +373,7 @@
togglePreventSelection(isPrevented = !!this.setting.tabSelectsMatch) {
this.setting.tabSelectsMatch = !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);
/* 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 Turbolinks */
(function() {
((global) => {
var issuable_created;
issuable_created = false;
this.Issuable = {
global.Issuable = {
init: function() {
Issuable.initTemplates();
Issuable.initSearch();
......@@ -111,7 +111,11 @@
filterResults: (function(_this) {
return function(form) {
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');
issuesUrl = formAction;
issuesUrl += "" + (formAction.indexOf('?') < 0 ? '?' : '&');
......@@ -184,4 +188,4 @@
}
};
}).call(this);
})(window);
......@@ -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() {
return $('body').data('page').split(':')[0];
};
......
......@@ -59,16 +59,13 @@
class MergeRequestTabs {
constructor({ action, setUrl, buildsLoaded, stubLocation } = {}) {
constructor({ action, setUrl, stubLocation } = {}) {
this.diffsLoaded = false;
this.buildsLoaded = false;
this.pipelinesLoaded = false;
this.commitsLoaded = false;
this.fixedLayoutPref = null;
this.setUrl = setUrl !== undefined ? setUrl : true;
this.buildsLoaded = buildsLoaded || false;
this.setCurrentAction = this.setCurrentAction.bind(this);
this.tabShown = this.tabShown.bind(this);
this.showTab = this.showTab.bind(this);
......@@ -119,10 +116,6 @@
$.scrollTo('.merge-request-details .merge-request-tabs', {
offset: -navBarHeight,
});
} else if (action === 'builds') {
this.loadBuilds($target.attr('href'));
this.expandView();
this.resetViewContainer();
} else if (action === 'pipelines') {
this.loadPipelines($target.attr('href'));
this.expandView();
......@@ -180,8 +173,8 @@
setCurrentAction(action) {
this.currentAction = action === 'show' ? 'notes' : action;
// Remove a trailing '/commits' '/diffs' '/builds' '/pipelines' '/new' '/new/diffs'
let newState = location.pathname.replace(/\/(commits|diffs|builds|pipelines|new|new\/diffs)(\.html)?\/?$/, '');
// Remove a trailing '/commits' '/diffs' '/pipelines' '/new' '/new/diffs'
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'
if (this.currentAction !== 'notes') {
......@@ -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) {
if (this.pipelinesLoaded) {
return;
......
......@@ -74,7 +74,7 @@
MergeRequestWidget.prototype.addEventListeners = function() {
var allowedPages;
allowedPages = ['show', 'commits', 'builds', 'pipelines', 'changes'];
allowedPages = ['show', 'commits', 'pipelines', 'changes'];
$(document).on('page:change.merge_request', (function(_this) {
return function() {
var page;
......@@ -190,7 +190,6 @@
message = message.replace('{{title}}', data.title);
notify(title, message, _this.opts.gitlab_icon, function() {
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 @@
@import "framework/asciidoctor.scss";
@import "framework/blocks.scss";
@import "framework/buttons.scss";
@import "framework/badges.scss";
@import "framework/calendar.scss";
@import "framework/callout.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 @@
padding: 20px;
color: $gl-gray;
font-weight: normal;
font-size: 16px;
font-size: 14px;
line-height: 36px;
&.diff-collapsed {
......
......@@ -76,13 +76,6 @@
color: $black;
}
}
.badge {
font-weight: normal;
background-color: $nav-badge-bg;
color: $gl-gray-light;
vertical-align: baseline;
}
}
&.sub-nav {
......
......@@ -33,7 +33,7 @@
@import "bootstrap/labels";
@import "bootstrap/badges";
@import "bootstrap/alerts";
// @import "bootstrap/progress-bars";
@import "bootstrap/progress-bars";
@import "bootstrap/list-group";
@import "bootstrap/wells";
@import "bootstrap/close";
......
......@@ -136,8 +136,8 @@ $md-area-border: #ddd;
/*
* Code
*/
$code_font_size: 13px;
$code_line_height: 1.5;
$code_font_size: 12px;
$code_line_height: 1.6;
/*
* Padding
......@@ -286,6 +286,14 @@ $btn-active-gray: #ececec;
$btn-active-gray-light: e4e7ed;
$btn-white-active: #848484;
/*
* Badges
*/
$badge-bg: #f3f3f3;
$badge-bg-dark: #eee;
$badge-color: #929292;
$badge-color-dark: #8f8f8f;
/*
* 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 {
pre.trace {
background: $builds-trace-bg;
......@@ -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 {
width: 31px;
margin: 0 0 0 auto;
}
&.affix-bottom {
position: absolute;
.scroll-link,
.autoscroll-container {
right: 25px;
z-index: 1;
}
&.affix {
right: 25px;
bottom: 15px;
z-index: 1;
.scroll-link {
position: fixed;
display: block;
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 {
right: #{$gutter_width + ($gl-padding * 2)};
&.scroll-top:hover .gitlab-icon-scroll-up-hover,
&.scroll-bottom:hover .gitlab-icon-scroll-down-hover {
display: inline-block;
}
a {
display: block;
margin-bottom: 10px;
&.scroll-top {
top: 110px;
}
&.scroll-bottom {
bottom: -2px;
}
}
.environment-information {
background-color: $gray-light;
border: 1px solid $border-color;
padding: 12px $gl-padding;
border-radius: $border-radius-default;
.autoscroll-container {
position: absolute;
}
svg {
position: relative;
top: 1px;
margin-right: 5px;
&.sidebar-expanded {
.scroll-link,
.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 {
position: relative;
padding: 0;
......@@ -109,6 +194,15 @@
.bash {
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 {
......@@ -248,6 +342,12 @@
}
}
.build-sidebar {
.container-fluid.container-limited {
max-width: 100%;
}
}
.build-detail-row {
margin-bottom: 5px;
......
......@@ -121,13 +121,6 @@
.folder-name {
cursor: pointer;
.badge {
font-weight: normal;
background-color: $gray-darker;
color: $gl-gray-light;
vertical-align: baseline;
}
}
}
......
......@@ -434,6 +434,7 @@
.issuable-meta {
display: inline-block;
line-height: 18px;
font-size: 14px;
}
.js-issuable-selector-wrap {
......
......@@ -21,6 +21,14 @@
display: inline-block;
float: left;
.btn-success.dropdown-toggle .fa {
color: inherit;
}
.btn-success.dropdown-toggle:disabled {
background-color: $gl-success;
}
.accept_merge_request {
&.ci-pending,
&.ci-running {
......@@ -96,7 +104,7 @@
.mr-widget-body {
h4 {
font-weight: 600;
font-size: 17px;
font-size: 16px;
margin: 5px 0;
color: $gl-gray-dark;
......
......@@ -43,7 +43,7 @@ ul.notes {
}
.system-note-message {
display: inline-block;
display: inline;
&::first-letter {
text-transform: lowercase;
......@@ -55,7 +55,7 @@ ul.notes {
}
p {
display: inline-block;
display: inline;
margin: 0;
&::first-letter {
......@@ -151,6 +151,10 @@ ul.notes {
}
}
}
.note-headline-light {
display: inline;
}
}
.discussion-body {
......
......@@ -80,6 +80,10 @@
td {
padding: 10px 8px;
}
.commit-link {
padding: 9px 8px 10px;
}
}
tbody {
......@@ -193,7 +197,7 @@
width: 8px;
position: absolute;
right: -7px;
bottom: 9px;
bottom: 10px;
border-bottom: 2px solid $border-color;
}
}
......@@ -499,15 +503,10 @@
> .ci-action-icon-container {
position: absolute;
right: 4px;
right: 5px;
top: 5px;
}
.ci-status-icon {
position: relative;
top: 1px;
}
.ci-status-icon svg {
height: 20px;
width: 20px;
......@@ -614,6 +613,10 @@
a {
display: inline-block;
}
.build-content {
width: 138px;
&:hover {
background-color: $stage-hover-bg;
......@@ -623,15 +626,24 @@
ul {
max-height: 245px;
overflow: auto;
margin: 5px 0;
margin: 3px 0;
li {
padding-top: 2px;
margin: 0 5px;
margin: 4px 7px;
padding: 0 3px;
padding-left: 0;
padding-bottom: 0;
margin-bottom: 0;
line-height: 1.2;
line-height: 0;
.ci-action-icon-container:hover {
background-color: transparent;
}
.ci-status-icon {
position: relative;
top: 2px;
}
}
}
}
......@@ -680,11 +692,15 @@
.dropdown-build {
color: $gl-text-color-light;
.build-content {
padding: 3px 7px 6px;
}
.ci-action-icon-container {
padding: 0;
font-size: 11px;
float: right;
margin-top: 4px;
margin-top: 3px;
display: inline-block;
position: relative;
......@@ -694,16 +710,10 @@
}
}
&:hover {
background-color: $stage-hover-bg;
border-radius: 3px;
color: $gl-text-color;
}
.ci-action-icon-container {
i {
width: 25px;
height: 25px;
width: 24px;
height: 24px;
&::before {
top: 1px;
......@@ -740,6 +750,10 @@
margin: 0;
}
.dropdown-build .build-content {
padding: 3px 7px 7px;
}
.builds-dropdown-loading {
margin: 10px auto;
width: 18px;
......@@ -788,26 +802,32 @@
.mini-pipeline-graph-icon-container .ci-status-icon {
display: inline-block;
border: 1px solid;
border-radius: 20px;
border-radius: 22px;
margin-right: 1px;
width: 20px;
height: 20px;
width: 22px;
height: 22px;
position: relative;
z-index: 2;
transition: all 0.2s cubic-bezier(0.25, 0, 1, 1);
svg {
top: -1px;
left: -1px;
}
}
.stage-cell .mini-pipeline-graph-icon-container .ci-status-icon svg {
width: 22px;
height: 22px;
}
.builds-dropdown {
&:focus {
outline: none;
margin-right: -8px;
.ci-status-icon {
width: 28px;
width: 32px;
padding: 0 8px 0 0;
transition: width 0.2s cubic-bezier(0.25, 0, 1, 1);
......@@ -851,7 +871,7 @@
.mini-pipeline-graph-icon-container {
.ci-status-icon:hover,
.ci-status-icon:focus {
width: 28px;
width: 32px;
padding: 0 8px 0 0;
+ .dropdown-caret {
......@@ -863,7 +883,7 @@
font-size: 11px;
position: relative;
top: 3px;
left: -11px;
left: -14px;
margin-right: -6px;
display: none;
z-index: 2;
......
......@@ -929,3 +929,23 @@ a.allowed-to-push {
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 {
.ci-status {
padding: 2px 7px;
padding: 2px 7px 4px;
margin-right: 10px;
border: 1px solid $gray-darker;
white-space: nowrap;
......
......@@ -82,6 +82,8 @@ class GroupsController < Groups::ApplicationController
if Groups::UpdateService.new(@group, current_user, group_params).execute
redirect_to edit_group_path(@group), notice: "Group '#{@group.name}' was successfully updated."
else
@group.reset_path!
render action: "edit"
end
end
......
......@@ -8,13 +8,11 @@ class Projects::CommitController < Projects::ApplicationController
# Authorize
before_action :require_non_empty_project
before_action :authorize_download_code!, except: [:cancel_builds, :retry_builds]
before_action :authorize_update_build!, only: [:cancel_builds, :retry_builds]
before_action :authorize_download_code!
before_action :authorize_read_pipeline!, only: [:pipelines]
before_action :authorize_read_commit_status!, only: [:builds]
before_action :commit
before_action :define_commit_vars, only: [:show, :diff_for_path, :builds, :pipelines]
before_action :define_status_vars, only: [:show, :builds, :pipelines]
before_action :define_commit_vars, only: [:show, :diff_for_path, :pipelines]
before_action :define_status_vars, only: [:show, :pipelines]
before_action :define_note_vars, only: [:show, :diff_for_path]
before_action :authorize_edit_tree!, only: [:revert, :cherry_pick]
......@@ -35,25 +33,6 @@ class Projects::CommitController < Projects::ApplicationController
def pipelines
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
@branches = @project.repository.branch_names_contains(commit.id)
@tags = @project.repository.tag_names_contains(commit.id)
......@@ -98,10 +77,6 @@ class Projects::CommitController < Projects::ApplicationController
@noteable = @commit ||= @project.commit(params[:id])
end
def ci_builds
@ci_builds ||= Ci::Build.where(pipeline: pipelines)
end
def define_commit_vars
return git_not_found! unless commit
......@@ -133,8 +108,6 @@ class Projects::CommitController < Projects::ApplicationController
def define_status_vars
@ci_pipelines = project.pipelines.where(sha: commit.sha)
@statuses = CommitStatus.where(pipeline: @ci_pipelines).relevant
@builds = Ci::Build.where(pipeline: @ci_pipelines).relevant
end
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
before_action :module_enabled
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,
: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_widget_vars, only: [:merge, :cancel_merge_when_build_succeeds, :merge_check]
before_action :define_commit_vars, only: [:diffs]
......@@ -203,17 +203,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController
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
@pipelines = @merge_request.all_pipelines
......
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
def ldap_enabled?
......
......@@ -188,7 +188,7 @@ module BlobHelper
end
def gitlab_ci_ymls
@gitlab_ci_ymls ||= Gitlab::Template::GitlabCiYmlTemplate.dropdown_names
@gitlab_ci_ymls ||= Gitlab::Template::GitlabCiYmlTemplate.dropdown_names(params[:context])
end
def dockerfile_names
......
module CiStatusHelper
def ci_status_path(pipeline)
project = pipeline.project
builds_namespace_project_commit_path(project.namespace, project, pipeline.sha)
namespace_project_pipeline_path(project.namespace, project, pipeline)
end
# 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
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(
project.namespace,
project,
project.default_branch || 'master',
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
......
......@@ -98,7 +98,7 @@ class Namespace < ActiveRecord::Base
def move_dir
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
# Move the namespace directory in all storages paths used by member projects
......@@ -111,7 +111,7 @@ class Namespace < ActiveRecord::Base
# 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')
raise Gitlab::UpdatePathError.new('namespace directory cannot be moved')
end
end
......
......@@ -18,4 +18,34 @@ class MattermostSlashCommandsService < ChatSlashCommandsService
def to_param
'mattermost_slash_commands'
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
......@@ -14,7 +14,13 @@ module Groups
group.assign_attributes(params)
begin
group.save
rescue Gitlab::UpdatePathError => e
group.errors.add(:base, e.message)
false
end
end
end
end
......@@ -14,7 +14,8 @@ class ProjectPathValidator < ActiveModel::EachValidator
# without tree as reserved name routing can match 'group/project' as group name,
# '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
preview blob blame raw files create_dir find_file]).freeze
......
......@@ -33,7 +33,7 @@
= link_to admin_abuse_reports_path, title: "Abuse Reports" do
%span
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
= link_to admin_license_path, title: 'License' do
......
......@@ -26,13 +26,13 @@
%span
Issues
- 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
= link_to merge_requests_group_path(@group), title: 'Merge Requests' do
%span
Merge Requests
- 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
= link_to group_group_members_path(@group), title: 'Members' do
%span
......
......@@ -61,14 +61,14 @@
%span
Issues
- 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
= nav_link(controller: :merge_requests) do
= link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do
%span
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
= nav_link(controller: :wikis) do
......
- ref = local_assigns.fetch(:ref)
- status = commit.status(ref)
- 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_label_for_status(status)
......
......@@ -7,7 +7,7 @@
= link_to pipeline_path(@build.pipeline) do
%strong ##{@build.pipeline.id}
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
from
= link_to namespace_project_commits_path(@project.namespace, @project, @build.ref) do
......
......@@ -56,17 +56,22 @@
- else
#js-build-scroll.scroll-controls
.scroll-step
= link_to '#build-trace', class: 'btn' do
%i.fa.fa-angle-up
= link_to '#down-build-trace', class: 'btn' do
%i.fa.fa-angle-down
%a{ href: '#up-build-trace', id: 'scroll-top', class: 'scroll-link scroll-top', title: 'Scroll to top' }
= custom_icon('scroll_up')
= custom_icon('scroll_up_hover_active')
%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?
.autoscroll-container
%button.btn.btn-sm#autoscroll-button{:type => "button", :data => {:state => 'disabled'}}
Enable autoscroll
%span.status-message#autoscroll-status{ data: { state: 'disabled' } }
%span.status-text Autoscroll active
%i.status-icon
= custom_icon('scroll_down_hover_active')
#up-build-trace
%pre.build-trace#build-trace
%code.bash.js-build-output
= icon("refresh spin", class: "js-build-refresh")
.build-loader-animation.js-build-refresh
#down-build-trace
......
- @ci_pipelines.each do |pipeline|
= render "pipeline", pipeline: pipeline, pipeline_details: true
......@@ -8,7 +8,3 @@
= link_to pipelines_namespace_project_commit_path(@project.namespace, @project, @commit.id) do
Pipelines
%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 @@
= link_to url_for(params), data: {target: 'div#pipelines', action: 'pipelines', toggle: 'tab'} do
Pipelines
%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
= link_to url_for(params.merge(action: 'new_diffs')), data: {target: 'div#diffs', action: 'new/diffs', toggle: 'tab'} do
Changes
......@@ -49,9 +44,6 @@
= render "projects/merge_requests/show/commits"
#diffs.diffs.tab-pane
- # This tab is always loaded via AJAX
- if @pipeline.present?
#builds.builds.tab-pane
= render "projects/merge_requests/show/builds"
- if @pipelines.any?
#pipelines.pipelines.tab-pane
= render "projects/merge_requests/show/pipelines"
......@@ -66,6 +58,5 @@
});
:javascript
var merge_request = new MergeRequest({
action: "#{(@show_changes_tab ? 'new/diffs' : 'new')}",
buildsLoaded: "#{@pipeline.present? ? 'true' : 'false'}"
action: "#{(@show_changes_tab ? 'new/diffs' : 'new')}"
});
......@@ -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
Pipelines
%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
= link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#diffs', action: 'diffs', toggle: 'tab' } do
Changes
......@@ -99,8 +94,6 @@
#commits.commits.tab-pane
- # This tab is always loaded via AJAX
#builds.builds.tab-pane
- # This tab is always loaded via AJAX
#pipelines.pipelines.tab-pane
- # This tab is always loaded via AJAX
#diffs.diffs.tab-pane
......
= render "projects/commit/pipeline", pipeline: @pipeline, link_to_commit: true
......@@ -17,7 +17,7 @@
- # TODO, remove in later versions when services like Jenkins will set CI status via Commit status API
.mr-widget-heading
- %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)
%span
CI build
......
......@@ -24,12 +24,10 @@
preparing: "{{status}} build",
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)}"
};
if (typeof merge_request_widget !== 'undefined') {
clearInterval(merge_request_widget.fetchBuildStatusInterval);
merge_request_widget.cancelPolling();
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
= 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 @@
rows: 14, hint: true
= 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
= icon("spinner spin")
Checking ability to merge automatically&hellip;
:javascript
$(function() {
merge_request_widget.getMergeStatus();
});
......@@ -8,7 +8,6 @@
.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|
= render 'shared/service_settings', form: form, subject: @service
.footer-block.row-content-block
= form.submit 'Save changes', class: 'btn btn-save'
&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
This service allows GitLab users to perform common operations on this
......@@ -7,93 +7,9 @@
See list of available commands in Mattermost after setting up this service,
by entering
%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
%li
3. After adding the slash command, paste the
%strong token
into the field below
- if enabled
= render 'projects/services/mattermost_slash_commands/installation_info', subject: @service
.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 @@
- if koding_enabled? && @repository.koding_yml.blank?
%li.missing
= 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
.project-last-commit{ class: container_class }
......
......@@ -10,10 +10,10 @@
.text-content
- if has_button && current_user
%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
An issue can be a bug, a todo or a feature request that needs to be discussed in a project.
Besides, issues are searchable and filterable.
Issues can be bugs, tasks or ideas to be discussed.
Also, issues are searchable and filterable.
- if project_select_button
= render 'shared/new_project_item_select', path: 'issues/new', label: 'New issue'
- 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 @@
- show_menu_above = show_menu_above || false
- selected_text = selected.try(:title) || params[:milestone_title]
- 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)
= 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
......
---
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
# Enable the asset pipeline
config.assets.enabled = true
config.assets.paths << Gemojione.images_path
config.assets.paths << "vendor/assets/fonts"
config.assets.precompile << "*.png"
config.assets.precompile << "print.css"
config.assets.precompile << "notify.css"
......@@ -101,6 +102,7 @@ module Gitlab
config.assets.precompile << "protected_branches/protected_branches_bundle.js"
config.assets.precompile << "diff_notes/diff_notes_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 << "cycle_analytics/cycle_analytics_bundle.js"
config.assets.precompile << "merge_conflicts/merge_conflicts_bundle.js"
......@@ -112,6 +114,7 @@ module Gitlab
config.assets.precompile << "lib/utils/*.js"
config.assets.precompile << "lib/*.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
config.assets.version = '1.0'
......
......@@ -468,6 +468,16 @@ production: &base
# login_url: '/cas/login',
# service_validate_url: '/cas/p3/serviceValidate',
# 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',
# app_id: 'YOUR_APP_ID',
# app_secret: 'YOUR_APP_SECRET',
......
......@@ -32,10 +32,7 @@ constraints(ProjectUrlConstrainer.new) do
resources :commit, only: [:show], constraints: { id: /\h{7,40}/ } do
member do
get :branches
get :builds
get :pipelines
post :cancel_builds
post :retry_builds
post :revert
post :cherry_pick
get :diff_for_path
......@@ -82,6 +79,8 @@ constraints(ProjectUrlConstrainer.new) do
end
end
resource :mattermost, only: [:new, :create]
resources :deploy_keys, constraints: { id: /\d+/ }, only: [:index, :new, :create] do
member do
put :enable
......@@ -98,7 +97,6 @@ constraints(ProjectUrlConstrainer.new) do
get :diffs
get :conflicts
get :conflict_for_path
get :builds
get :pipelines
get :merge_check
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 @@
#
# 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
enable_extension "plpgsql"
......
......@@ -6,7 +6,7 @@ providers.
- [LDAP](ldap.md) Includes Active Directory, Apple Open Directory, Open LDAP,
and 389 Server
- [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
- [SAML](../../integration/saml.md) Configure GitLab as a SAML 2.0 Service Provider
- [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 @@
- [CI/CD pipelines settings](../user/project/pipelines/settings.md)
- [Review Apps](review_apps/index.md)
- [Git submodules](git_submodules.md) Using Git submodules in your CI jobs
- [Autodeploy](autodeploy/index.md)
## 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.
- [External issue tracker](external-issue-tracker.md) Redmine, JIRA, etc.
- [LDAP](ldap.md) Set up sign in via LDAP
- [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
- [CAS](cas.md) Configure GitLab to sign in using CAS
- [Kerberos](kerberos.md) Integrate with Kerberos
......
......@@ -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. |
| **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`. |
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:
```
......
......@@ -30,6 +30,7 @@ contains some settings that are common for all providers.
- [Crowd](crowd.md)
- [Azure](azure.md)
- [Auth0](auth0.md)
- [Authentiq](../administration/auth/authentiq.md)
## Initial OmniAuth Configuration
......
......@@ -47,8 +47,6 @@ Feature: Project Commits
And repository contains ".gitlab-ci.yml" file
When I click on commit link
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
Given I click on commit link
......
......@@ -166,15 +166,6 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps
expect(page).to have_content "Pipeline #1 for 570e7b2a pending"
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
fill_in 'commits-search', with: 'submodules'
end
......
......@@ -254,15 +254,26 @@ module Banzai
# Returns projects for the given paths.
def find_projects_for_paths(paths)
if RequestStore.active?
to_query = paths - project_refs_cache.keys
cache = project_refs_cache
to_query = paths - cache.keys
unless to_query.empty?
projects_relation_for_paths(to_query).each do |project|
get_or_set_cache(project_refs_cache, project.path_with_namespace) { project }
projects = projects_relation_for_paths(to_query)
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
project_refs_cache.slice(*paths).values
cache.slice(*paths).values.compact
else
projects_relation_for_paths(paths)
end
......
......@@ -10,7 +10,7 @@ module Banzai
node.set_attribute('href', href)
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('target', '_blank')
end
......
......@@ -60,7 +60,7 @@ module Ci
end
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!
else
not_found!
......
......@@ -70,8 +70,8 @@ module Gitlab
def tag_endpoint(trans, env)
endpoint = env[ENDPOINT_KEY]
path = endpoint_paths_cache[endpoint.route.route_method][endpoint.route.route_path]
trans.action = "Grape##{endpoint.route.route_method} #{path}"
path = endpoint_paths_cache[endpoint.route.request_method][endpoint.route.path]
trans.action = "Grape##{endpoint.route.request_method} #{path}"
end
private
......
......@@ -42,7 +42,7 @@ module Gitlab
key, value = parsed_field.first
if value.nil?
value = File.open(tmp_path)
value = open_file(tmp_path)
@open_files << value
else
value = decorate_params_value(value, @request.params[key], tmp_path)
......@@ -68,7 +68,7 @@ module Gitlab
case path_value
when nil
value_hash[path_key] = File.open(tmp_path)
value_hash[path_key] = open_file(tmp_path)
@open_files << value_hash[path_key]
value_hash
when Hash
......@@ -78,6 +78,10 @@ module Gitlab
raise "unexpected path value: #{path_value.inspect}"
end
end
def open_file(path)
::UploadedFile.new(path, File.basename(path), 'application/octet-stream')
end
end
def initialize(app)
......
......@@ -13,8 +13,9 @@ module Gitlab
def categories
{
"General" => '',
"Pages" => 'Pages'
'General' => '',
'Pages' => 'Pages',
'Autodeploy' => 'autodeploy'
}
end
......@@ -25,6 +26,11 @@ module Gitlab
def finder(project = nil)
Gitlab::Template::Finders::GlobalTemplateFinder.new(self.base_dir, self.extension, self.categories)
end
def dropdown_names(context)
categories = context == 'autodeploy' ? ['Autodeploy'] : ['General', 'Pages']
super().slice(*categories)
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
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
# instance with SSO configured where this GitLab instance is the provider.
#
......@@ -17,6 +24,8 @@ module Mattermost
include Doorkeeper::Helpers::Controller
include HTTParty
LEASE_TIMEOUT = 60
base_uri Settings.mattermost.host
attr_accessor :current_resource_owner, :token
......@@ -26,14 +35,18 @@ module Mattermost
end
def with_session
raise NoSessionError unless create
with_lease do
raise Mattermost::NoSessionError unless create
begin
yield self
rescue Errno::ECONNREFUSED
raise Mattermost::NoSessionError
ensure
destroy
end
end
end
# Next methods are needed for Doorkeeper
def pre_auth
......@@ -58,12 +71,16 @@ module Mattermost
end
def get(path, options = {})
handle_exceptions do
self.class.get(path, options.merge(headers: @headers))
end
end
def post(path, options = {})
handle_exceptions do
self.class.post(path, options.merge(headers: @headers))
end
end
private
......@@ -111,5 +128,33 @@ module Mattermost
response.headers['token']
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
module Mattermost
class Team < Client
def all
json_get('/api/v3/teams/all')
end
end
end
......@@ -105,4 +105,25 @@ describe GroupsController do
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
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
end
end
describe 'GET builds' do
it_behaves_like "loads labels", :builds
end
describe 'GET pipelines' do
it_behaves_like "loads labels", :pipelines
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
page.within '.timeline-content-form' do
note.native.send_keys('')
note.native.send_keys("~#{label.title[0]}")
sleep 1
note.click
end
......@@ -53,7 +52,6 @@ feature 'GFM autocomplete', feature: true, js: true do
page.within '.timeline-content-form' do
note.native.send_keys('')
note.native.send_keys("@#{user.username[0]}")
sleep 1
note.click
end
......@@ -67,7 +65,6 @@ feature 'GFM autocomplete', feature: true, js: true do
page.within '.timeline-content-form' do
note.native.send_keys('')
note.native.send_keys(":cartwheel")
sleep 1
note.click
end
......@@ -76,6 +73,22 @@ feature 'GFM autocomplete', feature: true, js: true do
expect_to_wrap(false, emoji_item, note, 'cartwheel_tone1')
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)
expect(item).to have_content(value)
expect(item).not_to have_content("\"#{value}\"")
......@@ -89,12 +102,4 @@ feature 'GFM autocomplete', feature: true, js: true do
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
......@@ -54,14 +54,14 @@ feature 'Merge request created from fork' do
scenario 'user visits a pipelines page', js: true do
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
expect(page).to have_content 'rspec'
expect(page).to have_content 'spinach'
expect(page).to have_content pipeline.status
expect(page).to have_content pipeline.id
end
expect(find_link('Cancel running')[:href])
expect(page.find('a.btn-remove')[:href])
.to include fork_project.path_with_namespace
end
end
......
require 'spec_helper'
feature 'project commit builds' do
feature 'project commit pipelines' do
given(:project) { create(:project) }
background do
......@@ -16,11 +16,13 @@ feature 'project commit builds' do
ref: 'master')
end
scenario 'user views commit builds page' do
visit builds_namespace_project_commit_path(project.namespace,
project, project.commit.sha)
scenario 'user views commit pipelines page' do
visit pipelines_namespace_project_commit_path(project.namespace, 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
......@@ -4,29 +4,26 @@ feature 'Setup Mattermost slash commands', feature: true do
include WaitForAjax
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:project) { create(:empty_project) }
let(:service) { project.create_mattermost_slash_commands_service }
let(:mattermost_enabled) { true }
before do
Settings.mattermost['enabled'] = mattermost_enabled
project.team << [user, :master]
login_as(user)
visit edit_namespace_project_service_path(project.namespace, project, service)
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
visit edit_namespace_project_service_path(project.namespace, project, service)
wait_for_ajax
expect(page).to have_content("This service allows GitLab users to perform common")
end
end
describe 'saving a token' do
let(:token) { ('a'..'z').to_a.join }
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
click_on 'Save'
......@@ -35,14 +32,21 @@ feature 'Setup Mattermost slash commands', feature: true do
expect(value).to eq(token)
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
describe 'the trigger url' do
it 'shows the correct url' do
visit edit_namespace_project_service_path(project.namespace, project, service)
describe 'mattermost service is not enabled' do
let(:mattermost_enabled) { false }
it 'shows the correct trigger url' do
value = find_field('request_url').value
expect(value).to match("api/v3/projects/#{project.id}/services/mattermost_slash_commands/trigger")
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
end
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
doc = Nokogiri::HTML.fragment('')
filter = described_class.new(doc, project: project)
expect(filter.find_projects_for_paths([project.path_with_namespace])).
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])).
to eq([project])
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
describe '#current_project_path' do
......
......@@ -80,4 +80,18 @@ describe Banzai::Filter::ExternalLinkFilter, lib: true do
expect(filter(act).to_html).to eq(exp)
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
......@@ -33,7 +33,7 @@ describe Gitlab::Metrics::RackMiddleware do
end
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)
env['api.endpoint'] = endpoint
......@@ -117,7 +117,7 @@ describe Gitlab::Metrics::RackMiddleware do
let(:transaction) { middleware.transaction_from_env(env) }
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)
env['api.endpoint'] = endpoint
......
......@@ -12,7 +12,7 @@ describe Gitlab::Middleware::Multipart do
expect(app).to receive(:call) do |env|
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)
end
......@@ -39,7 +39,7 @@ describe Gitlab::Middleware::Multipart do
expect(app).to receive(:call) do |env|
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)
end
......@@ -54,7 +54,7 @@ describe Gitlab::Middleware::Multipart do
expect(app).to receive(:call) do |env|
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)
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
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
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'
describe MattermostSlashCommandsService, :models do
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
......@@ -37,6 +37,11 @@ describe Ci::API::Builds do
let(:user_agent) { 'Go-http-client/1.1' }
it { expect(response).to have_http_status(404) }
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
context 'when there is a pending build' do
......
......@@ -9,7 +9,7 @@ describe Groups::UpdateService, services: true do
describe "#execute" do
context "project visibility_level validation" 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
public_group.add_user(user, Gitlab::Access::MASTER)
......@@ -23,7 +23,7 @@ describe Groups::UpdateService, services: true do
end
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
internal_group.add_user(user, Gitlab::Access::MASTER)
......@@ -39,7 +39,7 @@ describe Groups::UpdateService, services: true do
end
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
internal_group.add_user(user, Gitlab::Access::MASTER)
end
......@@ -49,4 +49,41 @@ describe Groups::UpdateService, services: true do
expect(internal_group.errors.count).to eq(1)
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
......@@ -35,8 +35,10 @@
1. Clone KaTeX. Anything later than 4fb9445a9 (is merged into master) will do.
2. make (requires node)
3. sed -i 's,fonts/,,' build/katex.css
4. Copy build/katex.js, build/katex.css and fonts/* to gitlab.
3. sed -e 's,fonts/,,' -e 's/url\(([^)]*)\)/url(font-path\1)/g' build/katex.css > build/katex.scss
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){
......
......@@ -35,119 +35,121 @@ SOFTWARE.
1. Clone KaTeX. Anything later than 4fb9445a9 (is merged into master) will do.
2. make (requires node)
3. sed -i 's,fonts/,,' build/katex.css
4. Copy build/katex.js, build/katex.css and fonts/* to gitlab.
3. sed -e 's,fonts/,,' -e 's/url\(([^)]*)\)/url(font-path\1)/g' build/katex.css > build/katex.scss
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-family: 'KaTeX_AMS';
src: url('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'));
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-style: normal;
}
@font-face {
font-family: 'KaTeX_Caligraphic';
src: url('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'));
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-style: normal;
}
@font-face {
font-family: 'KaTeX_Caligraphic';
src: url('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'));
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-style: normal;
}
@font-face {
font-family: 'KaTeX_Fraktur';
src: url('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'));
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-style: normal;
}
@font-face {
font-family: 'KaTeX_Fraktur';
src: url('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'));
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-style: normal;
}
@font-face {
font-family: 'KaTeX_Main';
src: url('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'));
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-style: normal;
}
@font-face {
font-family: 'KaTeX_Main';
src: url('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'));
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-style: italic;
}
@font-face {
font-family: 'KaTeX_Main';
src: url('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'));
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-style: normal;
}
@font-face {
font-family: 'KaTeX_Math';
src: url('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'));
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-style: italic;
}
@font-face {
font-family: 'KaTeX_SansSerif';
src: url('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'));
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-style: normal;
}
@font-face {
font-family: 'KaTeX_Script';
src: url('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'));
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-style: normal;
}
@font-face {
font-family: 'KaTeX_Size1';
src: url('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'));
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-style: normal;
}
@font-face {
font-family: 'KaTeX_Size2';
src: url('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'));
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-style: normal;
}
@font-face {
font-family: 'KaTeX_Size3';
src: url('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'));
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-style: normal;
}
@font-face {
font-family: 'KaTeX_Size4';
src: url('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'));
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-style: normal;
}
@font-face {
font-family: 'KaTeX_Typewriter';
src: url('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'));
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-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