Commit 61f65992 authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch 'master' into 'add-svg-loader'

# Conflicts:
#   app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6
parents 44622abe 883342ce
...@@ -20,7 +20,7 @@ gem 'rugged', '~> 0.24.0' ...@@ -20,7 +20,7 @@ gem 'rugged', '~> 0.24.0'
# Authentication libraries # Authentication libraries
gem 'devise', '~> 4.2' gem 'devise', '~> 4.2'
gem 'doorkeeper', '~> 4.2.0' gem 'doorkeeper', '~> 4.2.0'
gem 'omniauth', '~> 1.3.2' gem 'omniauth', '~> 1.4.2'
gem 'omniauth-auth0', '~> 1.4.1' gem 'omniauth-auth0', '~> 1.4.1'
gem 'omniauth-azure-oauth2', '~> 0.0.6' gem 'omniauth-azure-oauth2', '~> 0.0.6'
gem 'omniauth-cas3', '~> 1.1.2' gem 'omniauth-cas3', '~> 1.1.2'
......
...@@ -328,7 +328,7 @@ GEM ...@@ -328,7 +328,7 @@ GEM
temple (~> 0.7.6) temple (~> 0.7.6)
thor thor
tilt tilt
hashie (3.4.4) hashie (3.5.5)
health_check (2.2.1) health_check (2.2.1)
rails (>= 4.0) rails (>= 4.0)
hipchat (1.5.2) hipchat (1.5.2)
...@@ -441,7 +441,7 @@ GEM ...@@ -441,7 +441,7 @@ GEM
octokit (4.6.2) octokit (4.6.2)
sawyer (~> 0.8.0, >= 0.5.3) sawyer (~> 0.8.0, >= 0.5.3)
oj (2.17.4) oj (2.17.4)
omniauth (1.3.2) omniauth (1.4.2)
hashie (>= 1.2, < 4) hashie (>= 1.2, < 4)
rack (>= 1.0, < 3) rack (>= 1.0, < 3)
omniauth-auth0 (1.4.1) omniauth-auth0 (1.4.1)
...@@ -920,7 +920,7 @@ DEPENDENCIES ...@@ -920,7 +920,7 @@ DEPENDENCIES
oauth2 (~> 1.2.0) oauth2 (~> 1.2.0)
octokit (~> 4.6.2) octokit (~> 4.6.2)
oj (~> 2.17.4) oj (~> 2.17.4)
omniauth (~> 1.3.2) omniauth (~> 1.4.2)
omniauth-auth0 (~> 1.4.1) omniauth-auth0 (~> 1.4.1)
omniauth-authentiq (~> 0.3.0) omniauth-authentiq (~> 0.3.0)
omniauth-azure-oauth2 (~> 0.0.6) omniauth-azure-oauth2 (~> 0.0.6)
......
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, max-len, one-var, one-var-declaration-per-line, quotes, prefer-template, newline-per-chained-call, comma-dangle, new-cap, no-else-return, consistent-return */ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, max-len, one-var, one-var-declaration-per-line, quotes, prefer-template, newline-per-chained-call, comma-dangle, new-cap, no-else-return, consistent-return */
/* global FilesCommentButton */ /* global FilesCommentButton */
/* global notes */
(function() { (function() {
let $commentButtonTemplate;
var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
this.FilesCommentButton = (function() { this.FilesCommentButton = (function() {
var COMMENT_BUTTON_CLASS, COMMENT_BUTTON_TEMPLATE, DEBOUNCE_TIMEOUT_DURATION, EMPTY_CELL_CLASS, LINE_COLUMN_CLASSES, LINE_CONTENT_CLASS, LINE_HOLDER_CLASS, LINE_NUMBER_CLASS, OLD_LINE_CLASS, TEXT_FILE_SELECTOR, UNFOLDABLE_LINE_CLASS; var COMMENT_BUTTON_CLASS, EMPTY_CELL_CLASS, LINE_COLUMN_CLASSES, LINE_CONTENT_CLASS, LINE_HOLDER_CLASS, LINE_NUMBER_CLASS, OLD_LINE_CLASS, TEXT_FILE_SELECTOR, UNFOLDABLE_LINE_CLASS;
COMMENT_BUTTON_CLASS = '.add-diff-note'; COMMENT_BUTTON_CLASS = '.add-diff-note';
COMMENT_BUTTON_TEMPLATE = _.template('<button name="button" type="submit" class="btn <%- COMMENT_BUTTON_CLASS %> js-add-diff-note-button" title="Add a comment to this line"><i class="fa fa-comment-o"></i></button>');
LINE_HOLDER_CLASS = '.line_holder'; LINE_HOLDER_CLASS = '.line_holder';
LINE_NUMBER_CLASS = 'diff-line-num'; LINE_NUMBER_CLASS = 'diff-line-num';
...@@ -27,26 +27,29 @@ ...@@ -27,26 +27,29 @@
TEXT_FILE_SELECTOR = '.text-file'; TEXT_FILE_SELECTOR = '.text-file';
DEBOUNCE_TIMEOUT_DURATION = 100;
function FilesCommentButton(filesContainerElement) { function FilesCommentButton(filesContainerElement) {
var debounce;
this.filesContainerElement = filesContainerElement;
this.destroy = bind(this.destroy, this);
this.render = bind(this.render, this); this.render = bind(this.render, this);
this.VIEW_TYPE = $('input#view[type=hidden]').val(); this.hideButton = bind(this.hideButton, this);
debounce = _.debounce(this.render, DEBOUNCE_TIMEOUT_DURATION); this.isParallelView = notes.isParallelView();
$(this.filesContainerElement).off('mouseover', LINE_COLUMN_CLASSES).off('mouseleave', LINE_COLUMN_CLASSES).on('mouseover', LINE_COLUMN_CLASSES, debounce).on('mouseleave', LINE_COLUMN_CLASSES, this.destroy); filesContainerElement.on('mouseover', LINE_COLUMN_CLASSES, this.render)
.on('mouseleave', LINE_COLUMN_CLASSES, this.hideButton);
} }
FilesCommentButton.prototype.render = function(e) { FilesCommentButton.prototype.render = function(e) {
var $currentTarget, buttonParentElement, lineContentElement, textFileElement; var $currentTarget, buttonParentElement, lineContentElement, textFileElement, $button;
$currentTarget = $(e.currentTarget); $currentTarget = $(e.currentTarget);
buttonParentElement = this.getButtonParent($currentTarget);
if (!this.validateButtonParent(buttonParentElement)) return;
lineContentElement = this.getLineContent($currentTarget); lineContentElement = this.getLineContent($currentTarget);
if (!this.validateLineContent(lineContentElement)) return; buttonParentElement = this.getButtonParent($currentTarget);
if (!this.validateButtonParent(buttonParentElement) || !this.validateLineContent(lineContentElement)) return;
$button = $(COMMENT_BUTTON_CLASS, buttonParentElement);
buttonParentElement.addClass('is-over')
.nextUntil(`.${LINE_CONTENT_CLASS}`).addClass('is-over');
if ($button.length) {
return;
}
textFileElement = this.getTextFileElement($currentTarget); textFileElement = this.getTextFileElement($currentTarget);
buttonParentElement.append(this.buildButton({ buttonParentElement.append(this.buildButton({
...@@ -61,19 +64,16 @@ ...@@ -61,19 +64,16 @@
})); }));
}; };
FilesCommentButton.prototype.destroy = function(e) { FilesCommentButton.prototype.hideButton = function(e) {
if (this.isMovingToSameType(e)) { var $currentTarget = $(e.currentTarget);
return; var buttonParentElement = this.getButtonParent($currentTarget);
}
$(COMMENT_BUTTON_CLASS, this.getButtonParent($(e.currentTarget))).remove(); buttonParentElement.removeClass('is-over')
.nextUntil(`.${LINE_CONTENT_CLASS}`).removeClass('is-over');
}; };
FilesCommentButton.prototype.buildButton = function(buttonAttributes) { FilesCommentButton.prototype.buildButton = function(buttonAttributes) {
var initializedButtonTemplate; return $commentButtonTemplate.clone().attr({
initializedButtonTemplate = COMMENT_BUTTON_TEMPLATE({
COMMENT_BUTTON_CLASS: COMMENT_BUTTON_CLASS.substr(1)
});
return $(initializedButtonTemplate).attr({
'data-noteable-type': buttonAttributes.noteableType, 'data-noteable-type': buttonAttributes.noteableType,
'data-noteable-id': buttonAttributes.noteableID, 'data-noteable-id': buttonAttributes.noteableID,
'data-commit-id': buttonAttributes.commitID, 'data-commit-id': buttonAttributes.commitID,
...@@ -86,14 +86,14 @@ ...@@ -86,14 +86,14 @@
}; };
FilesCommentButton.prototype.getTextFileElement = function(hoveredElement) { FilesCommentButton.prototype.getTextFileElement = function(hoveredElement) {
return $(hoveredElement.closest(TEXT_FILE_SELECTOR)); return hoveredElement.closest(TEXT_FILE_SELECTOR);
}; };
FilesCommentButton.prototype.getLineContent = function(hoveredElement) { FilesCommentButton.prototype.getLineContent = function(hoveredElement) {
if (hoveredElement.hasClass(LINE_CONTENT_CLASS)) { if (hoveredElement.hasClass(LINE_CONTENT_CLASS)) {
return hoveredElement; return hoveredElement;
} }
if (this.VIEW_TYPE === 'inline') { if (!this.isParallelView) {
return $(hoveredElement).closest(LINE_HOLDER_CLASS).find("." + LINE_CONTENT_CLASS); return $(hoveredElement).closest(LINE_HOLDER_CLASS).find("." + LINE_CONTENT_CLASS);
} else { } else {
return $(hoveredElement).next("." + LINE_CONTENT_CLASS); return $(hoveredElement).next("." + LINE_CONTENT_CLASS);
...@@ -101,7 +101,7 @@ ...@@ -101,7 +101,7 @@
}; };
FilesCommentButton.prototype.getButtonParent = function(hoveredElement) { FilesCommentButton.prototype.getButtonParent = function(hoveredElement) {
if (this.VIEW_TYPE === 'inline') { if (!this.isParallelView) {
if (hoveredElement.hasClass(OLD_LINE_CLASS)) { if (hoveredElement.hasClass(OLD_LINE_CLASS)) {
return hoveredElement; return hoveredElement;
} }
...@@ -114,17 +114,8 @@ ...@@ -114,17 +114,8 @@
} }
}; };
FilesCommentButton.prototype.isMovingToSameType = function(e) {
var newButtonParent;
newButtonParent = this.getButtonParent($(e.toElement));
if (!newButtonParent) {
return false;
}
return newButtonParent.is(this.getButtonParent($(e.currentTarget)));
};
FilesCommentButton.prototype.validateButtonParent = function(buttonParentElement) { FilesCommentButton.prototype.validateButtonParent = function(buttonParentElement) {
return !buttonParentElement.hasClass(EMPTY_CELL_CLASS) && !buttonParentElement.hasClass(UNFOLDABLE_LINE_CLASS) && $(COMMENT_BUTTON_CLASS, buttonParentElement).length === 0; return !buttonParentElement.hasClass(EMPTY_CELL_CLASS) && !buttonParentElement.hasClass(UNFOLDABLE_LINE_CLASS);
}; };
FilesCommentButton.prototype.validateLineContent = function(lineContentElement) { FilesCommentButton.prototype.validateLineContent = function(lineContentElement) {
...@@ -135,6 +126,8 @@ ...@@ -135,6 +126,8 @@
})(); })();
$.fn.filesCommentButton = function() { $.fn.filesCommentButton = function() {
$commentButtonTemplate = $('<button name="button" type="submit" class="add-diff-note js-add-diff-note-button" title="Add a comment to this line"><i class="fa fa-comment-o"></i></button>');
if (!(this && (this.parent().data('can-create-note') != null))) { if (!(this && (this.parent().data('can-create-note') != null))) {
return; return;
} }
......
...@@ -285,5 +285,58 @@ ...@@ -285,5 +285,58 @@
* @returns {Boolean} * @returns {Boolean}
*/ */
w.gl.utils.convertPermissionToBoolean = permission => permission === 'true'; w.gl.utils.convertPermissionToBoolean = permission => permission === 'true';
/**
* Back Off exponential algorithm
* backOff :: (Function<next, stop>, Number) -> Promise<Any, Error>
*
* @param {Function<next, stop>} fn function to be called
* @param {Number} timeout
* @return {Promise<Any, Error>}
* @example
* ```
* backOff(function (next, stop) {
* // Let's perform this function repeatedly for 60s or for the timeout provided.
*
* ourFunction()
* .then(function (result) {
* // continue if result is not what we need
* next();
*
* // when result is what we need let's stop with the repetions and jump out of the cycle
* stop(result);
* })
* .catch(function (error) {
* // if there is an error, we need to stop this with an error.
* stop(error);
* })
* }, 60000)
* .then(function (result) {})
* .catch(function (error) {
* // deal with errors passed to stop()
* })
* ```
*/
w.gl.utils.backOff = (fn, timeout = 60000) => {
const maxInterval = 32000;
let nextInterval = 2000;
const startTime = Date.now();
return new Promise((resolve, reject) => {
const stop = arg => ((arg instanceof Error) ? reject(arg) : resolve(arg));
const next = () => {
if (Date.now() - startTime < timeout) {
setTimeout(fn.bind(null, next, stop), nextInterval);
nextInterval = Math.min(nextInterval + nextInterval, maxInterval);
} else {
reject(new Error('BACKOFF_TIMEOUT'));
}
};
fn(next, stop);
});
};
})(window); })(window);
}).call(window); }).call(window);
/**
* exports HTTP status codes
*/
const statusCodes = {
NO_CONTENT: 204,
OK: 200,
};
module.exports = statusCodes;
...@@ -84,13 +84,14 @@ ...@@ -84,13 +84,14 @@
} }
$(function() { $(function() {
$(document).on('focusout.ssh_key', '#key_key', function() { $(document).on('input.ssh_key', '#key_key', function() {
const $title = $('#key_title'); const $title = $('#key_title');
const comment = $(this).val().match(/^\S+ \S+ (.+)\n?$/); const comment = $(this).val().match(/^\S+ \S+ (.+)\n?$/);
if (comment && comment.length > 1 && $title.val() === '') {
// Extract the SSH Key title from its comment
if (comment && comment.length > 1) {
return $title.val(comment[1]).change(); return $title.val(comment[1]).change();
} }
// Extract the SSH Key title from its comment
}); });
if (global.utils.getPagePath() === 'profiles') { if (global.utils.getPagePath() === 'profiles') {
return new Profile(); return new Profile();
......
/* global Flash */
require('vendor/task_list'); require('vendor/task_list');
class TaskList { class TaskList {
...@@ -6,6 +7,16 @@ class TaskList { ...@@ -6,6 +7,16 @@ class TaskList {
this.dataType = options.dataType; this.dataType = options.dataType;
this.fieldName = options.fieldName; this.fieldName = options.fieldName;
this.onSuccess = options.onSuccess || (() => {}); this.onSuccess = options.onSuccess || (() => {});
this.onError = function showFlash(response) {
let errorMessages = '';
if (response.responseJSON) {
errorMessages = response.responseJSON.errors.join(' ');
}
return new Flash(errorMessages || 'Update failed', 'alert');
};
this.init(); this.init();
} }
...@@ -32,6 +43,7 @@ class TaskList { ...@@ -32,6 +43,7 @@ class TaskList {
url: $target.data('update-url') || $('form.js-issuable-update').attr('action'), url: $target.data('update-url') || $('form.js-issuable-update').attr('action'),
data: patchData, data: patchData,
success: this.onSuccess, success: this.onSuccess,
error: this.onError,
}); });
} }
} }
......
...@@ -39,17 +39,15 @@ const playIconSvg = require('icons/_icon_play.svg'); ...@@ -39,17 +39,15 @@ const playIconSvg = require('icons/_icon_play.svg');
template: ` template: `
<td class="pipeline-actions hidden-xs"> <td class="pipeline-actions hidden-xs">
<div class="controls pull-right"> <div class="pull-right">
<div class="btn-group inline"> <div class="btn-group">
<div class="btn-group"> <div class="btn-group" v-if="actions">
<button <button
v-if='actions'
class="dropdown-toggle btn btn-default has-tooltip js-pipeline-dropdown-manual-actions" class="dropdown-toggle btn btn-default has-tooltip js-pipeline-dropdown-manual-actions"
data-toggle="dropdown" data-toggle="dropdown"
title="Manual job" title="Manual job"
data-placement="top" data-placement="top"
aria-label="Manual job" aria-label="Manual job">
>
<span v-html="playIconSvg" aria-hidden="true"></span> <span v-html="playIconSvg" aria-hidden="true"></span>
<i class="fa fa-caret-down" aria-hidden="true"></i> <i class="fa fa-caret-down" aria-hidden="true"></i>
</button> </button>
...@@ -58,23 +56,21 @@ const playIconSvg = require('icons/_icon_play.svg'); ...@@ -58,23 +56,21 @@ const playIconSvg = require('icons/_icon_play.svg');
<a <a
rel="nofollow" rel="nofollow"
data-method="post" data-method="post"
:href='action.path' :href="action.path" >
>
<span v-html="playIconSvg" aria-hidden="true"></span> <span v-html="playIconSvg" aria-hidden="true"></span>
<span>{{action.name}}</span> <span>{{action.name}}</span>
</a> </a>
</li> </li>
</ul> </ul>
</div> </div>
<div class="btn-group">
<div class="btn-group" v-if="artifacts">
<button <button
v-if='artifacts'
class="dropdown-toggle btn btn-default build-artifacts has-tooltip js-pipeline-dropdown-download" class="dropdown-toggle btn btn-default build-artifacts has-tooltip js-pipeline-dropdown-download"
title="Artifacts" title="Artifacts"
data-placement="top" data-placement="top"
data-toggle="dropdown" data-toggle="dropdown"
aria-label="Artifacts" aria-label="Artifacts">
>
<i class="fa fa-download" aria-hidden="true"></i> <i class="fa fa-download" aria-hidden="true"></i>
<i class="fa fa-caret-down" aria-hidden="true"></i> <i class="fa fa-caret-down" aria-hidden="true"></i>
</button> </button>
...@@ -82,42 +78,39 @@ const playIconSvg = require('icons/_icon_play.svg'); ...@@ -82,42 +78,39 @@ const playIconSvg = require('icons/_icon_play.svg');
<li v-for='artifact in pipeline.details.artifacts'> <li v-for='artifact in pipeline.details.artifacts'>
<a <a
rel="nofollow" rel="nofollow"
download :href="artifact.path">
:href='artifact.path'
>
<i class="fa fa-download" aria-hidden="true"></i> <i class="fa fa-download" aria-hidden="true"></i>
<span>{{download(artifact.name)}}</span> <span>{{download(artifact.name)}}</span>
</a> </a>
</li> </li>
</ul> </ul>
</div> </div>
</div> <div class="btn-group" v-if="pipeline.flags.retryable">
<div class="cancel-retry-btns inline"> <a
<a class="btn btn-default btn-retry has-tooltip"
v-if='pipeline.flags.retryable' title="Retry"
class="btn has-tooltip" rel="nofollow"
title="Retry" data-method="post"
rel="nofollow" data-placement="top"
data-method="post" data-toggle="dropdown"
data-placement="top" :href='pipeline.retry_path'
data-toggle="dropdown" aria-label="Retry">
:href='pipeline.retry_path' <i class="fa fa-repeat" aria-hidden="true"></i>
aria-label="Retry"> </a>
<i class="fa fa-repeat" aria-hidden="true"></i> </div>
</a> <div class="btn-group" v-if="pipeline.flags.cancelable">
<a <a
v-if='pipeline.flags.cancelable' class="btn btn-remove has-tooltip"
@click="confirmAction" title="Cancel"
class="btn btn-remove has-tooltip" rel="nofollow"
title="Cancel" data-method="post"
rel="nofollow" data-placement="top"
data-method="post" data-toggle="dropdown"
data-placement="top" :href='pipeline.cancel_path'
data-toggle="dropdown" aria-label="Cancel">
:href='pipeline.cancel_path' <i class="fa fa-remove" aria-hidden="true"></i>
aria-label="Cancel"> </a>
<i class="fa fa-remove" aria-hidden="true"></i> </div>
</a>
</div> </div>
</div> </div>
</td> </td>
......
...@@ -57,7 +57,7 @@ const iconTimerSvg = require('../../../views/shared/icons/_icon_timer.svg'); ...@@ -57,7 +57,7 @@ const iconTimerSvg = require('../../../views/shared/icons/_icon_timer.svg');
}, },
}, },
template: ` template: `
<td> <td class="pipelines-time-ago">
<p class="duration" v-if='duration'> <p class="duration" v-if='duration'>
<span v-html="iconTimerSvg"></span> <span v-html="iconTimerSvg"></span>
{{duration}} {{duration}}
...@@ -68,8 +68,7 @@ const iconTimerSvg = require('../../../views/shared/icons/_icon_timer.svg'); ...@@ -68,8 +68,7 @@ const iconTimerSvg = require('../../../views/shared/icons/_icon_timer.svg');
data-toggle="tooltip" data-toggle="tooltip"
data-placement="top" data-placement="top"
data-container="body" data-container="body"
:data-original-title='localTimeFinished' :data-original-title='localTimeFinished'>
>
{{timeStopped.words}} {{timeStopped.words}}
</time> </time>
</p> </p>
......
...@@ -229,7 +229,7 @@ ...@@ -229,7 +229,7 @@
.controls { .controls {
float: right; float: right;
margin-top: 8px; margin-top: 8px;
padding-bottom: 7px; padding-bottom: 8px;
border-bottom: 1px solid $border-color; border-bottom: 1px solid $border-color;
} }
} }
......
...@@ -20,6 +20,7 @@ $dark-highlight-bg: #ffe792; ...@@ -20,6 +20,7 @@ $dark-highlight-bg: #ffe792;
$dark-highlight-color: $black; $dark-highlight-color: $black;
$dark-pre-hll-bg: #373b41; $dark-pre-hll-bg: #373b41;
$dark-hll-bg: #373b41; $dark-hll-bg: #373b41;
$dark-over-bg: #9f9ab5;
$dark-c: #969896; $dark-c: #969896;
$dark-err: #c66; $dark-err: #c66;
$dark-k: #b294bb; $dark-k: #b294bb;
...@@ -139,6 +140,18 @@ $dark-il: #de935f; ...@@ -139,6 +140,18 @@ $dark-il: #de935f;
} }
} }
.diff-line-num {
&.is-over,
&.hll:not(.empty-cell).is-over {
background-color: $dark-over-bg;
border-color: darken($dark-over-bg, 5%);
a {
color: darken($dark-over-bg, 15%);
}
}
}
.line_content.match { .line_content.match {
@include dark-diff-match-line; @include dark-diff-match-line;
} }
......
...@@ -13,6 +13,7 @@ $monokai-line-empty-bg: #49483e; ...@@ -13,6 +13,7 @@ $monokai-line-empty-bg: #49483e;
$monokai-line-empty-border: darken($monokai-line-empty-bg, 15%); $monokai-line-empty-border: darken($monokai-line-empty-bg, 15%);
$monokai-diff-border: #808080; $monokai-diff-border: #808080;
$monokai-highlight-bg: #ffe792; $monokai-highlight-bg: #ffe792;
$monokai-over-bg: #9f9ab5;
$monokai-new-bg: rgba(166, 226, 46, 0.1); $monokai-new-bg: rgba(166, 226, 46, 0.1);
$monokai-new-idiff: rgba(166, 226, 46, 0.15); $monokai-new-idiff: rgba(166, 226, 46, 0.15);
...@@ -139,6 +140,18 @@ $monokai-gi: #a6e22e; ...@@ -139,6 +140,18 @@ $monokai-gi: #a6e22e;
} }
} }
.diff-line-num {
&.is-over,
&.hll:not(.empty-cell).is-over {
background-color: $monokai-over-bg;
border-color: darken($monokai-over-bg, 5%);
a {
color: darken($monokai-over-bg, 15%);
}
}
}
.line_content.match { .line_content.match {
@include dark-diff-match-line; @include dark-diff-match-line;
} }
......
...@@ -17,6 +17,7 @@ $solarized-dark-line-color-new: #5a766c; ...@@ -17,6 +17,7 @@ $solarized-dark-line-color-new: #5a766c;
$solarized-dark-line-color-old: #7a6c71; $solarized-dark-line-color-old: #7a6c71;
$solarized-dark-highlight: #094554; $solarized-dark-highlight: #094554;
$solarized-dark-hll-bg: #174652; $solarized-dark-hll-bg: #174652;
$solarized-dark-over-bg: #9f9ab5;
$solarized-dark-c: #586e75; $solarized-dark-c: #586e75;
$solarized-dark-err: #93a1a1; $solarized-dark-err: #93a1a1;
$solarized-dark-g: #93a1a1; $solarized-dark-g: #93a1a1;
...@@ -143,6 +144,18 @@ $solarized-dark-il: #2aa198; ...@@ -143,6 +144,18 @@ $solarized-dark-il: #2aa198;
} }
} }
.diff-line-num {
&.is-over,
&.hll:not(.empty-cell).is-over {
background-color: $solarized-dark-over-bg;
border-color: darken($solarized-dark-over-bg, 5%);
a {
color: darken($solarized-dark-over-bg, 15%);
}
}
}
.line_content.match { .line_content.match {
@include dark-diff-match-line; @include dark-diff-match-line;
} }
......
...@@ -18,6 +18,7 @@ $solarized-light-line-color-new: #a1a080; ...@@ -18,6 +18,7 @@ $solarized-light-line-color-new: #a1a080;
$solarized-light-line-color-old: #ad9186; $solarized-light-line-color-old: #ad9186;
$solarized-light-highlight: #eee8d5; $solarized-light-highlight: #eee8d5;
$solarized-light-hll-bg: #ddd8c5; $solarized-light-hll-bg: #ddd8c5;
$solarized-light-over-bg: #ded7fc;
$solarized-light-c: #93a1a1; $solarized-light-c: #93a1a1;
$solarized-light-err: #586e75; $solarized-light-err: #586e75;
$solarized-light-g: #586e75; $solarized-light-g: #586e75;
...@@ -150,6 +151,18 @@ $solarized-light-il: #2aa198; ...@@ -150,6 +151,18 @@ $solarized-light-il: #2aa198;
} }
} }
.diff-line-num {
&.is-over,
&.hll:not(.empty-cell).is-over {
background-color: $solarized-light-over-bg;
border-color: darken($solarized-light-over-bg, 5%);
a {
color: darken($solarized-light-over-bg, 15%);
}
}
}
.line_content.match { .line_content.match {
@include matchLine; @include matchLine;
} }
......
...@@ -7,6 +7,7 @@ $white-code-color: $gl-text-color; ...@@ -7,6 +7,7 @@ $white-code-color: $gl-text-color;
$white-highlight: #fafe3d; $white-highlight: #fafe3d;
$white-pre-hll-bg: #f8eec7; $white-pre-hll-bg: #f8eec7;
$white-hll-bg: #f8f8f8; $white-hll-bg: #f8f8f8;
$white-over-bg: #ded7fc;
$white-c: #998; $white-c: #998;
$white-err: #a61717; $white-err: #a61717;
$white-err-bg: #e3d2d2; $white-err-bg: #e3d2d2;
...@@ -123,6 +124,16 @@ $white-gc-bg: #eaf2f5; ...@@ -123,6 +124,16 @@ $white-gc-bg: #eaf2f5;
} }
} }
&.is-over,
&.hll:not(.empty-cell).is-over {
background-color: $white-over-bg;
border-color: darken($white-over-bg, 5%);
a {
color: darken($white-over-bg, 15%);
}
}
&.hll:not(.empty-cell) { &.hll:not(.empty-cell) {
background-color: $line-number-select; background-color: $line-number-select;
border-color: $line-select-yellow-dark; border-color: $line-select-yellow-dark;
......
...@@ -89,6 +89,10 @@ ...@@ -89,6 +89,10 @@
.diff-line-num { .diff-line-num {
width: 50px; width: 50px;
a {
transition: none;
}
} }
.line_holder td { .line_holder td {
...@@ -109,10 +113,6 @@ ...@@ -109,10 +113,6 @@
td.line_content.parallel { td.line_content.parallel {
width: 46%; width: 46%;
} }
.add-diff-note {
margin-left: -65px;
}
} }
.old_line, .old_line,
......
...@@ -452,36 +452,37 @@ ul.notes { ...@@ -452,36 +452,37 @@ ul.notes {
* Line note button on the side of diffs * Line note button on the side of diffs
*/ */
.diff-file tr.line_holder { .add-diff-note {
@mixin show-add-diff-note { display: none;
display: inline-block; margin-top: -2px;
} border-radius: 50%;
background: $white-light;
padding: 1px 5px;
font-size: 12px;
color: $gl-link-color;
margin-left: -55px;
position: absolute;
z-index: 10;
width: 23px;
height: 23px;
border: 1px solid $border-color;
transition: transform .1s ease-in-out;
.add-diff-note { &:hover {
margin-top: -8px; background: $gl-info;
border-radius: 40px; color: $white-light;
background: $white-light; transform: scale(1.15);
padding: 4px; }
font-size: 16px;
color: $gl-link-color;
margin-left: -56px;
position: absolute;
z-index: 10;
width: 32px;
// "hide" it by default
display: none;
&:hover { &:active {
background: $gl-info; outline: 0;
color: $white-light;
@include show-add-diff-note;
}
} }
}
// "show" the icon also if we just hover somewhere over the line .diff-file {
&:hover > td { .is-over {
.add-diff-note { .add-diff-note {
@include show-add-diff-note; display: inline-block;
} }
} }
} }
......
...@@ -13,21 +13,16 @@ ...@@ -13,21 +13,16 @@
white-space: nowrap; white-space: nowrap;
} }
.commit-title { .table-holder {
margin: 0; width: 100%;
} overflow: auto;
.controls {
white-space: nowrap;
} }
.btn { .commit-title {
margin: 4px; margin: 0;
} }
.table.ci-table { .table.ci-table {
min-width: 1200px;
table-layout: fixed;
.label { .label {
margin-bottom: 3px; margin-bottom: 3px;
...@@ -37,16 +32,72 @@ ...@@ -37,16 +32,72 @@
color: $black; color: $black;
} }
.pipeline-date, .stage-cell {
.pipeline-status { min-width: 130px; // Guarantees we show at least 4 stages in line
width: 10%; width: 20%;
}
.pipelines-time-ago {
text-align: right;
} }
.pipeline-info,
.pipeline-commit,
.pipeline-stages,
.pipeline-actions { .pipeline-actions {
width: 20%; padding-right: 0;
min-width: 170px; //Guarantees buttons don't break in several lines.
.btn-default {
color: $gl-text-color-secondary;
}
.btn.btn-retry:hover,
.btn.btn-retry:focus {
border-color: $gray-darkest;
background-color: $white-normal;
}
svg path {
fill: $gl-text-color-secondary;
}
.dropdown-menu {
max-height: 250px;
overflow-y: auto;
}
.dropdown-toggle,
.dropdown-menu {
color: $gl-text-color-secondary;
.fa {
color: $gl-text-color-secondary;
font-size: 14px;
}
svg,
.fa {
margin-right: 0;
}
}
.btn-group {
&.open {
.btn-default {
background-color: $white-normal;
border-color: $border-white-normal;
}
}
.btn {
.icon-play {
height: 13px;
width: 12px;
}
}
}
.tooltip {
white-space: nowrap;
}
} }
} }
} }
...@@ -61,27 +112,10 @@ ...@@ -61,27 +112,10 @@
} }
} }
.content-list.pipelines .table-holder {
min-height: 300px;
}
.pipeline-holder {
width: 100%;
overflow: auto;
}
.table.ci-table { .table.ci-table {
min-width: 900px;
&.pipeline { &.builds-page tr {
min-width: 650px; height: 71px;
}
&.builds-page {
tr {
height: 71px;
}
} }
tr { tr {
...@@ -99,7 +133,7 @@ ...@@ -99,7 +133,7 @@
} }
.commit-link { .commit-link {
padding: 9px 8px 10px; padding: 9px 8px 10px 2px;
} }
} }
...@@ -206,72 +240,8 @@ ...@@ -206,72 +240,8 @@
} }
} }
.pipeline-actions { .build-link a {
min-width: 140px; color: $gl-text-color;
.btn {
margin: 0;
color: $gl-text-color-secondary;
}
.cancel-retry-btns {
vertical-align: middle;
.btn:not(:first-child) {
margin-left: 8px;
}
}
.dropdown-menu {
max-height: 250px;
overflow-y: auto;
}
.dropdown-toggle,
.dropdown-menu {
color: $gl-text-color-secondary;
.fa {
color: $gl-text-color-secondary;
font-size: 14px;
}
svg,
.fa {
margin-right: 0;
}
}
.btn-remove {
color: $white-light;
}
.btn-group {
&.open {
.btn-default {
background-color: $white-normal;
border-color: $border-white-normal;
}
}
.btn {
.icon-play {
height: 13px;
width: 12px;
}
}
}
.tooltip {
white-space: nowrap;
}
}
.build-link {
a {
color: $gl-text-color;
}
} }
.btn-group.open .dropdown-toggle { .btn-group.open .dropdown-toggle {
...@@ -335,31 +305,8 @@ ...@@ -335,31 +305,8 @@
} }
.tab-pane { .tab-pane {
&.pipelines { &.builds .ci-table tr {
.ci-table { height: 71px;
min-width: 900px;
}
.content-list.pipelines {
overflow: auto;
}
.stage {
max-width: 100px;
width: 100px;
}
.pipeline-actions {
min-width: initial;
}
}
&.builds {
.ci-table {
tr {
height: 71px;
}
}
} }
} }
......
...@@ -638,14 +638,6 @@ pre.light-well { ...@@ -638,14 +638,6 @@ pre.light-well {
margin: 0; margin: 0;
} }
.activity-filter-block {
.controls {
padding-bottom: 7px;
margin-top: 8px;
border-bottom: 1px solid $border-color;
}
}
.commits-search-form { .commits-search-form {
.input-short { .input-short {
min-width: 200px; min-width: 200px;
......
...@@ -26,6 +26,23 @@ module IssuableActions ...@@ -26,6 +26,23 @@ module IssuableActions
private private
def render_conflict_response
respond_to do |format|
format.html do
@conflict = true
render :edit
end
format.json do
render json: {
errors: [
"Someone edited this #{issuable.human_class_name} at the same time you did. Please refresh your browser and make sure your changes will not unintentionally remove theirs."
]
}, status: 409
end
end
end
def labels def labels
@labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute @labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute
end end
......
...@@ -33,6 +33,7 @@ module ServiceParams ...@@ -33,6 +33,7 @@ module ServiceParams
:issues_url, :issues_url,
:jira_issue_transition_id, :jira_issue_transition_id,
:merge_requests_events, :merge_requests_events,
:mock_service_url,
:namespace, :namespace,
:new_issue_url, :new_issue_url,
:notify, :notify,
......
...@@ -134,8 +134,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -134,8 +134,7 @@ class Projects::IssuesController < Projects::ApplicationController
end end
rescue ActiveRecord::StaleObjectError rescue ActiveRecord::StaleObjectError
@conflict = true render_conflict_response
render :edit
end end
def referenced_merge_requests def referenced_merge_requests
......
...@@ -296,22 +296,21 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -296,22 +296,21 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def update def update
@merge_request = MergeRequests::UpdateService.new(project, current_user, merge_request_params).execute(@merge_request) @merge_request = MergeRequests::UpdateService.new(project, current_user, merge_request_params).execute(@merge_request)
if @merge_request.valid? respond_to do |format|
respond_to do |format| format.html do
format.html do if @merge_request.valid?
redirect_to([@merge_request.target_project.namespace.becomes(Namespace), redirect_to([@merge_request.target_project.namespace.becomes(Namespace), @merge_request.target_project, @merge_request])
@merge_request.target_project, @merge_request]) else
end render :edit
format.json do
render json: @merge_request.to_json(include: { milestone: {}, assignee: { methods: :avatar_url }, labels: { methods: :text_color } }, methods: [:task_status, :task_status_short])
end end
end end
else
render "edit" format.json do
render json: @merge_request.to_json(include: { milestone: {}, assignee: { methods: :avatar_url }, labels: { methods: :text_color } }, methods: [:task_status, :task_status_short])
end
end end
rescue ActiveRecord::StaleObjectError rescue ActiveRecord::StaleObjectError
@conflict = true render_conflict_response
render :edit
end end
def remove_wip def remove_wip
......
...@@ -34,7 +34,7 @@ module ButtonHelper ...@@ -34,7 +34,7 @@ module ButtonHelper
content_tag (append_link ? :a : :span), protocol, content_tag (append_link ? :a : :span), protocol,
class: klass, class: klass,
href: (project.http_url_to_repo if append_link), href: (project.http_url_to_repo(current_user) if append_link),
data: { data: {
html: true, html: true,
placement: placement, placement: placement,
......
...@@ -241,7 +241,7 @@ module ProjectsHelper ...@@ -241,7 +241,7 @@ module ProjectsHelper
when 'ssh' when 'ssh'
project.ssh_url_to_repo project.ssh_url_to_repo
else else
project.http_url_to_repo project.http_url_to_repo(current_user)
end end
end end
......
...@@ -36,7 +36,7 @@ class Event < ActiveRecord::Base ...@@ -36,7 +36,7 @@ class Event < ActiveRecord::Base
scope :code_push, -> { where(action: PUSHED) } scope :code_push, -> { where(action: PUSHED) }
scope :in_projects, ->(projects) do scope :in_projects, ->(projects) do
where(project_id: projects).recent where(project_id: projects.pluck(:id)).recent
end end
scope :with_associations, -> { includes(:author, :project, project: :namespace).preload(:target) } scope :with_associations, -> { includes(:author, :project, project: :namespace).preload(:target) }
......
...@@ -869,8 +869,14 @@ class Project < ActiveRecord::Base ...@@ -869,8 +869,14 @@ class Project < ActiveRecord::Base
url_to_repo url_to_repo
end end
def http_url_to_repo def http_url_to_repo(user = nil)
"#{web_url}.git" url = web_url
if user
url.sub!(%r{\Ahttps?://}) { |protocol| "#{protocol}#{user.username}@" }
end
"#{url}.git"
end end
# Check if current branch name is marked as protected in the system # Check if current branch name is marked as protected in the system
......
...@@ -15,10 +15,10 @@ class MattermostService < ChatNotificationService ...@@ -15,10 +15,10 @@ class MattermostService < ChatNotificationService
'This service sends notifications about projects events to Mattermost channels.<br /> 'This service sends notifications about projects events to Mattermost channels.<br />
To set up this service: To set up this service:
<ol> <ol>
<li><a href="https://docs.mattermost.com/developer/webhooks-incoming.html#enabling-incoming-webhooks">Enable incoming webhooks</a> in your Mattermost installation. </li> <li><a href="https://docs.mattermost.com/developer/webhooks-incoming.html#enabling-incoming-webhooks">Enable incoming webhooks</a> in your Mattermost installation.</li>
<li><a href="https://docs.mattermost.com/developer/webhooks-incoming.html#creating-integrations-using-incoming-webhooks">Add an incoming webhook</a> in your Mattermost team. The default channel can be overridden for each event. </li> <li><a href="https://docs.mattermost.com/developer/webhooks-incoming.html#creating-integrations-using-incoming-webhooks">Add an incoming webhook</a> in your Mattermost team. The default channel can be overridden for each event.</li>
<li>Paste the webhook <strong>URL</strong> into the field bellow. </li> <li>Paste the webhook <strong>URL</strong> into the field below.</li>
<li>Select events below to enable notifications. The channel and username are optional. </li> <li>Select events below to enable notifications. The <strong>Channel handle</strong> and <strong>Username</strong> fields are optional.</li>
</ol>' </ol>'
end end
...@@ -28,14 +28,14 @@ class MattermostService < ChatNotificationService ...@@ -28,14 +28,14 @@ class MattermostService < ChatNotificationService
def default_fields def default_fields
[ [
{ type: 'text', name: 'webhook', placeholder: 'http://mattermost_host/hooks/...' }, { type: 'text', name: 'webhook', placeholder: 'e.g. http://mattermost_host/hooks/…' },
{ type: 'text', name: 'username', placeholder: 'username' }, { type: 'text', name: 'username', placeholder: 'e.g. GitLab' },
{ type: 'checkbox', name: 'notify_only_broken_builds' }, { type: 'checkbox', name: 'notify_only_broken_builds' },
{ type: 'checkbox', name: 'notify_only_broken_pipelines' }, { type: 'checkbox', name: 'notify_only_broken_pipelines' },
] ]
end end
def default_channel_placeholder def default_channel_placeholder
"town-square" "Channel handle (e.g. town-square)"
end end
end end
# For an example companion mocking service, see https://gitlab.com/gitlab-org/gitlab-mock-ci-service
class MockCiService < CiService
ALLOWED_STATES = %w[failed canceled running pending success success_with_warnings skipped not_found].freeze
prop_accessor :mock_service_url
validates :mock_service_url, presence: true, url: true, if: :activated?
def title
'MockCI'
end
def description
'Mock an external CI'
end
def self.to_param
'mock_ci'
end
def fields
[
{ type: 'text',
name: 'mock_service_url',
placeholder: 'http://localhost:4004' },
]
end
# Return complete url to build page
#
# Ex.
# http://jenkins.example.com:8888/job/test1/scm/bySHA1/12d65c
#
def build_page(sha, ref)
url = [mock_service_url,
"#{project.namespace.path}/#{project.path}/status/#{sha}"]
URI.join(*url).to_s
end
# Return string with build status or :error symbol
#
# Allowed states: 'success', 'failed', 'running', 'pending', 'skipped'
#
#
# Ex.
# @service.commit_status('13be4ac', 'master')
# # => 'success'
#
# @service.commit_status('2abe4ac', 'dev')
# # => 'running'
#
#
def commit_status(sha, ref)
response = HTTParty.get(commit_status_path(sha), verify: false)
read_commit_status(response)
rescue Errno::ECONNREFUSED
:error
end
def commit_status_path(sha)
url = [mock_service_url,
"#{project.namespace.path}/#{project.path}/status/#{sha}.json"]
URI.join(*url).to_s
end
def read_commit_status(response)
return :error unless response.code == 200 || response.code == 404
status = if response.code == 404
'pending'
else
response['status']
end
if status.present? && ALLOWED_STATES.include?(status)
status
else
:error
end
end
end
...@@ -13,11 +13,11 @@ class SlackService < ChatNotificationService ...@@ -13,11 +13,11 @@ class SlackService < ChatNotificationService
def help def help
'This service sends notifications about projects events to Slack channels.<br /> 'This service sends notifications about projects events to Slack channels.<br />
To setup this service: To set up this service:
<ol> <ol>
<li><a href="https://slack.com/apps/A0F7XDUAZ-incoming-webhooks">Add an incoming webhook</a> in your Slack team. The default channel can be overridden for each event. </li> <li><a href="https://slack.com/apps/A0F7XDUAZ-incoming-webhooks">Add an incoming webhook</a> in your Slack team. The default channel can be overridden for each event.</li>
<li>Paste the <strong>Webhook URL</strong> into the field below. </li> <li>Paste the <strong>Webhook URL</strong> into the field below.</li>
<li>Select events below to enable notifications. The channel and username are optional. </li> <li>Select events below to enable notifications. The <strong>Channel name</strong> and <strong>Username</strong> fields are optional.</li>
</ol>' </ol>'
end end
...@@ -27,14 +27,14 @@ class SlackService < ChatNotificationService ...@@ -27,14 +27,14 @@ class SlackService < ChatNotificationService
def default_fields def default_fields
[ [
{ type: 'text', name: 'webhook', placeholder: 'https://hooks.slack.com/services/...' }, { type: 'text', name: 'webhook', placeholder: 'e.g. https://hooks.slack.com/services/…' },
{ type: 'text', name: 'username', placeholder: 'username' }, { type: 'text', name: 'username', placeholder: 'e.g. GitLab' },
{ type: 'checkbox', name: 'notify_only_broken_builds' }, { type: 'checkbox', name: 'notify_only_broken_builds' },
{ type: 'checkbox', name: 'notify_only_broken_pipelines' }, { type: 'checkbox', name: 'notify_only_broken_pipelines' },
] ]
end end
def default_channel_placeholder def default_channel_placeholder
"#general" "Channel name (e.g. general)"
end end
end end
...@@ -109,9 +109,7 @@ class Repository ...@@ -109,9 +109,7 @@ class Repository
offset: offset, offset: offset,
after: after, after: after,
before: before, before: before,
# --follow doesn't play well with --skip. See: follow: path.present?,
# https://gitlab.com/gitlab-org/gitlab-ce/issues/3574#note_3040520
follow: false,
skip_merges: skip_merges skip_merges: skip_merges
} }
......
...@@ -210,7 +210,7 @@ class Service < ActiveRecord::Base ...@@ -210,7 +210,7 @@ class Service < ActiveRecord::Base
end end
def self.available_services_names def self.available_services_names
%w[ service_names = %w[
asana asana
assembla assembla
bamboo bamboo
...@@ -238,6 +238,9 @@ class Service < ActiveRecord::Base ...@@ -238,6 +238,9 @@ class Service < ActiveRecord::Base
slack slack
teamcity teamcity
] ]
service_names << 'mock_ci' if Rails.env.development?
service_names.sort_by(&:downcase)
end end
def self.build_from_template(project_id, template) def self.build_from_template(project_id, template)
......
...@@ -18,7 +18,8 @@ module Groups ...@@ -18,7 +18,8 @@ module Groups
end end
group.children.each do |group| group.children.each do |group|
DestroyService.new(group, current_user).async_execute # This needs to be synchronous since the namespace gets destroyed below
DestroyService.new(group, current_user).execute
end end
group.really_destroy! group.really_destroy!
......
...@@ -14,6 +14,8 @@ ...@@ -14,6 +14,8 @@
= runner.short_sha = runner.short_sha
%td %td
= runner.description = runner.description
%td
= runner.version
%td %td
- if runner.shared? - if runner.shared?
n/a n/a
......
...@@ -67,6 +67,7 @@ ...@@ -67,6 +67,7 @@
%th Type %th Type
%th Runner token %th Runner token
%th Description %th Description
%th Version
%th Projects %th Projects
%th Jobs %th Jobs
%th Tags %th Tags
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
.nav-block .nav-block
- if current_user - if current_user
.controls .controls
= link_to dashboard_projects_path(:atom, { private_token: current_user.private_token }), class: 'btn rss-btn' do = link_to dashboard_projects_path(:atom, { private_token: current_user.private_token }), class: 'btn rss-btn has-tooltip', title: 'Subscribe' do
%i.fa.fa-rss %i.fa.fa-rss
= render 'shared/event_filter' = render 'shared/event_filter'
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
.nav-block .nav-block
- if current_user - if current_user
.controls .controls
= link_to group_path(@group, format: :atom, private_token: current_user.private_token), class: 'btn rss-btn' do = link_to group_path(@group, format: :atom, private_token: current_user.private_token), class: 'btn rss-btn has-tooltip' , title: 'Subscribe' do
%i.fa.fa-rss %i.fa.fa-rss
= render 'shared/event_filter' = render 'shared/event_filter'
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
.nav-block.activity-filter-block .nav-block.activity-filter-block
- if current_user - if current_user
.controls .controls
= link_to namespace_project_path(@project.namespace, @project, format: :atom, private_token: current_user.private_token), title: "Feed", class: 'btn rss-btn' do = link_to namespace_project_path(@project.namespace, @project, format: :atom, private_token: current_user.private_token), title: "Subscribe", class: 'btn rss-btn has-tooltip' do
= icon('rss') = icon('rss')
= render 'shared/event_filter' = render 'shared/event_filter'
......
- status = pipeline.status
- show_commit = local_assigns.fetch(:show_commit, true)
- show_branch = local_assigns.fetch(:show_branch, true)
%tr.commit
%td.commit-link
= render 'ci/status/badge', status: pipeline.detailed_status(current_user)
%td
= link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id) do
%span.pipeline-id ##{pipeline.id}
%span by
- if pipeline.user
= user_avatar(user: pipeline.user, size: 20)
- else
%span.api.monospace API
- if pipeline.latest?
%span.label.label-success.has-tooltip{ title: 'Latest pipeline for this branch' } latest
- if pipeline.triggered?
%span.label.label-primary triggered
- if pipeline.yaml_errors.present?
%span.label.label-danger.has-tooltip{ title: "#{pipeline.yaml_errors}" } yaml invalid
- if pipeline.builds.any?(&:stuck?)
%span.label.label-warning stuck
%td.branch-commit
- if pipeline.ref && show_branch
.icon-container
= pipeline.tag? ? icon('tag') : icon('code-fork')
= link_to pipeline.ref, namespace_project_commits_path(pipeline.project.namespace, pipeline.project, pipeline.ref), class: "monospace branch-name"
- if show_commit
.icon-container.commit-icon
= custom_icon("icon_commit")
= link_to pipeline.short_sha, namespace_project_commit_path(pipeline.project.namespace, pipeline.project, pipeline.sha), class: "commit-id monospace"
%p.commit-title
- if commit = pipeline.commit
= author_avatar(commit, size: 20)
= link_to_gfm truncate(commit.title, length: 60, escape: false), namespace_project_commit_path(pipeline.project.namespace, pipeline.project, commit.id), class: "commit-row-message"
- else
Cant find HEAD commit for this branch
%td
= render 'shared/mini_pipeline_graph', pipeline: pipeline, klass: 'js-mini-pipeline-graph'
%td
- if pipeline.duration
%p.duration
= custom_icon("icon_timer")
= duration_in_numbers(pipeline.duration)
- if pipeline.finished_at
%p.finished-at
= icon("calendar")
#{time_ago_with_tooltip(pipeline.finished_at, short_format: false)}
%td.pipeline-actions.hidden-xs
.controls.pull-right
- artifacts = pipeline.builds.latest.with_artifacts_not_expired
- actions = pipeline.manual_actions
- if artifacts.present? || actions.any?
.btn-group.inline
- if actions.any?
.btn-group
%button.dropdown-toggle.btn.btn-default.has-tooltip.js-pipeline-dropdown-manual-actions{ type: 'button', title: 'Manual pipeline', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label' => 'Manual pipeline' }
= custom_icon('icon_play')
= icon('caret-down', 'aria-hidden' => 'true')
%ul.dropdown-menu.dropdown-menu-align-right
- actions.each do |build|
%li
= link_to play_namespace_project_build_path(pipeline.project.namespace, pipeline.project, build), method: :post, rel: 'nofollow' do
= custom_icon('icon_play')
%span= build.name
- if artifacts.present?
.btn-group
%button.dropdown-toggle.btn.btn-default.build-artifacts.has-tooltip.js-pipeline-dropdown-download{ type: 'button', title: 'Artifacts', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label' => 'Artifacts' }
= icon("download")
= icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
- artifacts.each do |build|
%li
= link_to download_namespace_project_build_artifacts_path(pipeline.project.namespace, pipeline.project, build), rel: 'nofollow', download: '' do
= icon("download")
%span Download '#{build.name}' artifacts
- if can?(current_user, :update_pipeline, pipeline.project)
.cancel-retry-btns.inline
- if pipeline.retryable?
= link_to retry_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn has-tooltip', title: 'Retry', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label' => 'Retry' , method: :post do
= icon("repeat")
- if pipeline.cancelable?
= link_to cancel_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn btn-remove has-tooltip', title: 'Cancel', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label' => 'Cancel' , method: :post do
= icon("remove")
---
title: Add runner version to /admin/runners view
merge_request: 8733
author: Jonathon Reinhart
---
title: Add the Username to the HTTP(S) clone URL of a Repository
merge_request: 9347
author: Jan Christophersen
---
title: Make Git history follow renames again by performing the --skip in Ruby
merge_request:
author:
---
title: 'Add performance query regression fix for !9088 affecting #27267'
merge_request:
author:
---
title: Add Runner's registration/deletion v4 API
merge_request: 9246
author:
---
title: Fix issuable stale object error handler for js when updating tasklists
merge_request:
author:
---
title: Add Mock CI service/integration for development
merge_request:
author:
---
title: Improved diff comment button UX
merge_request:
author:
---
title: Fixed RSS button alignment on activity pages
merge_request:
author:
---
title: Bump Hashie to 3.5.5 and omniauth to 1.4.2 to eliminate warning noise
merge_request:
author:
---
title: SSH key field updates title after pasting key
merge_request:
author:
---
title: 'API: Return 400 for all validation erros in the mebers API'
merge_request: 9523
author: Robert Schilling
---
title: update Vue to v2.1.10
merge_request: 9386
author:
...@@ -92,7 +92,7 @@ var config = { ...@@ -92,7 +92,7 @@ var config = {
'emoji-aliases$': path.join(ROOT_PATH, 'fixtures/emojis/aliases.json'), 'emoji-aliases$': path.join(ROOT_PATH, 'fixtures/emojis/aliases.json'),
'icons': path.join(ROOT_PATH, 'app/views/shared/icons'), 'icons': path.join(ROOT_PATH, 'app/views/shared/icons'),
'vendor': path.join(ROOT_PATH, 'vendor/assets/javascripts'), 'vendor': path.join(ROOT_PATH, 'vendor/assets/javascripts'),
'vue$': IS_PRODUCTION ? 'vue/dist/vue.min.js' : 'vue/dist/vue.js', 'vue$': 'vue/dist/vue.common.js',
} }
} }
} }
......
...@@ -466,6 +466,46 @@ If Registry is enabled in your GitLab instance, but you don't need it for your ...@@ -466,6 +466,46 @@ If Registry is enabled in your GitLab instance, but you don't need it for your
project, you can disable it from your project's settings. Read the user guide project, you can disable it from your project's settings. Read the user guide
on how to achieve that. on how to achieve that.
## Disable Container Registry but use GitLab as an auth endpoint
You can disable the embedded Container Registry to use an external one, but
still use GitLab as an auth endpoint.
**Omnibus GitLab**
1. Open `/etc/gitlab/gitlab.rb` and set necessary configurations:
```ruby
registry['enable'] = false
gitlab_rails['registry_enabled'] = true
gitlab_rails['registry_host'] = "registry.gitlab.example.com"
gitlab_rails['registry_port'] = "5005"
gitlab_rails['registry_api_url'] = "http://localhost:5000"
gitlab_rails['registry_key_path'] = "/var/opt/gitlab/gitlab-rails/certificate.key"
gitlab_rails['registry_path'] = "/var/opt/gitlab/gitlab-rails/shared/registry"
gitlab_rails['registry_issuer'] = "omnibus-gitlab-issuer"
```
1. Save the file and [reconfigure GitLab][] for the changes to take effect.
**Installations from source**
1. Open `/home/git/gitlab/config/gitlab.yml`, and edit the configuration settings under `registry`:
```
## Container Registry
registry:
enabled: true
host: "registry.gitlab.example.com"
port: "5005"
api_url: "http://localhost:5000"
path: /var/opt/gitlab/gitlab-rails/shared/registry
key: /var/opt/gitlab/gitlab-rails/certificate.key
issuer: omnibus-gitlab-issuer
```
1. Save the file and [restart GitLab][] for the changes to take effect.
## Storage limitations ## Storage limitations
Currently, there is no storage limitation, which means a user can upload an Currently, there is no storage limitation, which means a user can upload an
......
...@@ -810,3 +810,38 @@ GET /projects/:id/services/teamcity ...@@ -810,3 +810,38 @@ GET /projects/:id/services/teamcity
[jira-doc]: ../user/project/integrations/jira.md [jira-doc]: ../user/project/integrations/jira.md
[old-jira-api]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-13-stable/doc/api/services.md#jira [old-jira-api]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-13-stable/doc/api/services.md#jira
## MockCI
Mock an external CI. See [`gitlab-org/gitlab-mock-ci-service`](https://gitlab.com/gitlab-org/gitlab-mock-ci-service) for an example of a companion mock service.
This service is only available when your environment is set to development.
### Create/Edit MockCI service
Set MockCI service for a project.
```
PUT /projects/:id/services/mock-ci
```
Parameters:
- `mock_service_url` (**required**) - http://localhost:4004
### Delete MockCI service
Delete MockCI service for a project.
```
DELETE /projects/:id/services/mock-ci
```
### Get MockCI service settings
Get MockCI service settings for a project.
```
GET /projects/:id/services/mock-ci
```
...@@ -41,5 +41,6 @@ changes are in V4: ...@@ -41,5 +41,6 @@ changes are in V4:
- Renamed `branch_name` to `branch` on DELETE `id/repository/branches/:branch` response [!8936](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8936) - Renamed `branch_name` to `branch` on DELETE `id/repository/branches/:branch` response [!8936](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8936)
- Remove `public` param from create and edit actions of projects [!8736](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8736) - Remove `public` param from create and edit actions of projects [!8736](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8736)
- Notes do not return deprecated field `upvote` and `downvote` [!9384](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9384) - Notes do not return deprecated field `upvote` and `downvote` [!9384](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9384)
- Return HTTP status code `400` for all validation errors when creating or updating a member instead of sometimes `422` error. [!9523](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9523)
- Remove `GET /groups/owned`. Use `GET /groups?owned=true` instead [!9505](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9505) - Remove `GET /groups/owned`. Use `GET /groups?owned=true` instead [!9505](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9505)
- Return 202 with JSON body on async removals on V4 API (DELETE `/projects/:id/repository/merged_branches` and DELETE `/projects/:id`) [!9449](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9449) - Return 202 with JSON body on async removals on V4 API (DELETE `/projects/:id/repository/merged_branches` and DELETE `/projects/:id`) [!9449](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9449)
\ No newline at end of file
...@@ -1018,7 +1018,7 @@ A simple example: ...@@ -1018,7 +1018,7 @@ A simple example:
```yaml ```yaml
job1: job1:
coverage: /Code coverage: \d+\.\d+/ coverage: '/Code coverage: \d+\.\d+/'
``` ```
## Git Strategy ## Git Strategy
......
...@@ -2,11 +2,12 @@ ...@@ -2,11 +2,12 @@
This document describes what services we use for testing GitLab and GitLab CI. This document describes what services we use for testing GitLab and GitLab CI.
We currently use three CI services to test GitLab: We currently use four CI services to test GitLab:
1. GitLab CI on [GitHost.io](https://gitlab-ce.githost.io/projects/4/) for the [GitLab.com repo](https://gitlab.com/gitlab-org/gitlab-ce) 1. GitLab CI on [GitHost.io](https://gitlab-ce.githost.io/projects/4/) for the [GitLab.com repo](https://gitlab.com/gitlab-org/gitlab-ce)
2. GitLab CI at ci.gitlab.org to test the private GitLab B.V. repo at dev.gitlab.org 2. GitLab CI at ci.gitlab.org to test the private GitLab B.V. repo at dev.gitlab.org
3. [Semephore](https://semaphoreapp.com/gitlabhq/gitlabhq/) for [GitHub.com repo](https://github.com/gitlabhq/gitlabhq) 3. [Semephore](https://semaphoreapp.com/gitlabhq/gitlabhq/) for [GitHub.com repo](https://github.com/gitlabhq/gitlabhq)
4. [Mock CI Service](user/project/integrations/mock_ci.md) for local development
| Software @ configuration being tested | GitLab CI (ci.gitlab.org) | GitLab CI (GitHost.io) | Semaphore | | Software @ configuration being tested | GitLab CI (ci.gitlab.org) | GitLab CI (GitHost.io) | Semaphore |
|---------------------------------------|---------------------------|---------------------------------------------------------------------------|-----------| |---------------------------------------|---------------------------|---------------------------------------------------------------------------|-----------|
......
...@@ -24,23 +24,24 @@ There, you will see a checkbox with the following events that can be triggered: ...@@ -24,23 +24,24 @@ There, you will see a checkbox with the following events that can be triggered:
- Push - Push
- Issue - Issue
- Confidential issue
- Merge request - Merge request
- Note - Note
- Tag push - Tag push
- Build - Build
- Pipeline
- Wiki page - Wiki page
Bellow each of these event checkboxes, you will have an input field to insert Below each of these event checkboxes, you have an input field to enter
which Mattermost channel you want to send that event message, with `#town-square` which Mattermost channel you want to send that event message. Enter your preferred channel handle (the hash sign `#` is optional).
being the default. The hash sign is optional.
At the end, fill in your Mattermost details: At the end, fill in your Mattermost details:
| Field | Description | | Field | Description |
| ----- | ----------- | | ----- | ----------- |
| **Webhook** | The incoming webhooks which you have to setup on Mattermost, it will be something like: http://mattermost.example/hooks/5xo... | | **Webhook** | The incoming webhook URL which you have to setup on Mattermost, it will be something like: http://mattermost.example/hooks/5xo… |
| **Username** | Optional username which can be on messages sent to Mattermost. Fill this in if you want to change the username of the bot. | | **Username** | Optional username which can be on messages sent to Mattermost. Fill this in if you want to change the username of the bot. |
| **Notify only broken builds** | If you choose to enable the **Build** event and you want to be only notified about failed builds. | | **Notify only broken builds** | If you choose to enable the **Build** event and you want to be only notified about failed builds. |
| **Notify only broken pipelines** | If you choose to enable the **Pipeline** event and you want to be only notified about failed pipelines. |
![Mattermost configuration](img/mattermost_configuration.png) ![Mattermost configuration](img/mattermost_configuration.png)
# Mock CI Service
**NB: This service is only listed if you are in a development environment!**
To setup the mock CI service server, respond to the following endpoints
- `commit_status`: `#{project.namespace.path}/#{project.path}/status/#{sha}.json`
- Have your service return `200 { status: ['failed'|'canceled'|'running'|'pending'|'success'|'success_with_warnings'|'skipped'|'not_found'] }`
- If the service returns a 404, it is interpreted as `pending`
- `build_page`: `#{project.namespace.path}/#{project.path}/status/#{sha}`
- Just where the build is linked to, doesn't matter if implemented
For an example of a mock CI server, see [`gitlab-org/gitlab-mock-ci-service`](https://gitlab.com/gitlab-org/gitlab-mock-ci-service)
...@@ -21,23 +21,25 @@ There, you will see a checkbox with the following events that can be triggered: ...@@ -21,23 +21,25 @@ There, you will see a checkbox with the following events that can be triggered:
- Push - Push
- Issue - Issue
- Confidential issue
- Merge request - Merge request
- Note - Note
- Tag push - Tag push
- Build - Build
- Pipeline
- Wiki page - Wiki page
Bellow each of these event checkboxes, you will have an input field to insert Below each of these event checkboxes, you have an input field to enter
which Slack channel you want to send that event message, with `#general` which Slack channel you want to send that event message. Enter your preferred channel name **without** the hash sign (`#`).
being the default. Enter your preferred channel **without** the hash sign (`#`).
At the end, fill in your Slack details: At the end, fill in your Slack details:
| Field | Description | | Field | Description |
| ----- | ----------- | | ----- | ----------- |
| **Webhook** | The [incoming webhook URL][slackhook] which you have to setup on Slack. | | **Webhook** | The [incoming webhook URL][slackhook] which you have to setup on Slack. |
| **Username** | Optional username which can be on messages sent to slack. Fill this in if you want to change the username of the bot. | | **Username** | Optional username which can be on messages sent to Slack. Fill this in if you want to change the username of the bot. |
| **Notify only broken builds** | If you choose to enable the **Build** event and you want to be only notified about failed builds. | | **Notify only broken builds** | If you choose to enable the **Build** event and you want to be only notified about failed builds. |
| **Notify only broken pipelines** | If you choose to enable the **Pipeline** event and you want to be only notified about failed pipelines. |
After you are all done, click **Save changes** for the changes to take effect. After you are all done, click **Save changes** for the changes to take effect.
......
...@@ -63,6 +63,12 @@ git commit -am "Added Debian iso" # commit the file meta data ...@@ -63,6 +63,12 @@ git commit -am "Added Debian iso" # commit the file meta data
git push origin master # sync the git repo and large file to the GitLab server git push origin master # sync the git repo and large file to the GitLab server
``` ```
>**Note**: Make sure that `.gitattributes` is tracked by git. Otherwise Git
LFS will not be working properly for people cloning the project.
```bash
git add .gitattributes
```
Cloning the repository works the same as before. Git automatically detects the Cloning the repository works the same as before. Git automatically detects the
LFS-tracked files and clones them via HTTP. If you performed the git clone LFS-tracked files and clones them via HTTP. If you performed the git clone
command with a SSH URL, you have to enter your GitLab credentials for HTTP command with a SSH URL, you have to enter your GitLab credentials for HTTP
......
...@@ -49,7 +49,7 @@ class Spinach::Features::ExploreProjects < Spinach::FeatureSteps ...@@ -49,7 +49,7 @@ class Spinach::Features::ExploreProjects < Spinach::FeatureSteps
step 'I should see an http link to the repository' do step 'I should see an http link to the repository' do
project = Project.find_by(name: 'Community') project = Project.find_by(name: 'Community')
expect(page).to have_field('project_clone', with: project.http_url_to_repo) expect(page).to have_field('project_clone', with: project.http_url_to_repo(@user))
end end
step 'I should see an ssh link to the repository' do step 'I should see an ssh link to the repository' do
......
...@@ -91,6 +91,7 @@ module API ...@@ -91,6 +91,7 @@ module API
mount ::API::Projects mount ::API::Projects
mount ::API::ProjectSnippets mount ::API::ProjectSnippets
mount ::API::Repositories mount ::API::Repositories
mount ::API::Runner
mount ::API::Runners mount ::API::Runners
mount ::API::Services mount ::API::Services
mount ::API::Session mount ::API::Session
......
...@@ -618,6 +618,10 @@ module API ...@@ -618,6 +618,10 @@ module API
end end
end end
class RunnerRegistrationDetails < Grape::Entity
expose :id, :token
end
class BuildArtifactFile < Grape::Entity class BuildArtifactFile < Grape::Entity
expose :filename, :size expose :filename, :size
end end
......
module API
module Helpers
module Runner
def runner_registration_token_valid?
ActiveSupport::SecurityUtils.variable_size_secure_compare(params[:token],
current_application_settings.runners_registration_token)
end
def get_runner_version_from_params
return unless params['info'].present?
attributes_for_keys(%w(name version revision platform architecture), params['info'])
end
def authenticate_runner!
forbidden! unless current_runner
end
def current_runner
@runner ||= ::Ci::Runner.find_by_token(params[:token].to_s)
end
end
end
end
...@@ -55,7 +55,6 @@ module API ...@@ -55,7 +55,6 @@ module API
authorize_admin_source!(source_type, source) authorize_admin_source!(source_type, source)
member = source.members.find_by(user_id: params[:user_id]) member = source.members.find_by(user_id: params[:user_id])
conflict!('Member already exists') if member conflict!('Member already exists') if member
member = source.add_user(params[:user_id], params[:access_level], current_user: current_user, expires_at: params[:expires_at]) member = source.add_user(params[:user_id], params[:access_level], current_user: current_user, expires_at: params[:expires_at])
...@@ -63,9 +62,6 @@ module API ...@@ -63,9 +62,6 @@ module API
if member.persisted? && member.valid? if member.persisted? && member.valid?
present member.user, with: Entities::Member, member: member present member.user, with: Entities::Member, member: member
else else
# This is to ensure back-compatibility but 400 behavior should be used
# for all validation errors in 9.0!
render_api_error!('Access level is not known', 422) if member.errors.key?(:access_level)
render_validation_error!(member) render_validation_error!(member)
end end
end end
...@@ -87,9 +83,6 @@ module API ...@@ -87,9 +83,6 @@ module API
if member.update_attributes(declared_params(include_missing: false)) if member.update_attributes(declared_params(include_missing: false))
present member.user, with: Entities::Member, member: member present member.user, with: Entities::Member, member: member
else else
# This is to ensure back-compatibility but 400 behavior should be used
# for all validation errors in 9.0!
render_api_error!('Access level is not known', 422) if member.errors.key?(:access_level)
render_validation_error!(member) render_validation_error!(member)
end end
end end
......
module API
class Runner < Grape::API
helpers ::API::Helpers::Runner
resource :runners do
desc 'Registers a new Runner' do
success Entities::RunnerRegistrationDetails
http_codes [[201, 'Runner was created'], [403, 'Forbidden']]
end
params do
requires :token, type: String, desc: 'Registration token'
optional :description, type: String, desc: %q(Runner's description)
optional :info, type: Hash, desc: %q(Runner's metadata)
optional :locked, type: Boolean, desc: 'Should Runner be locked for current project'
optional :run_untagged, type: Boolean, desc: 'Should Runner handle untagged jobs'
optional :tag_list, type: Array[String], desc: %q(List of Runner's tags)
end
post '/' do
attributes = attributes_for_keys [:description, :locked, :run_untagged, :tag_list]
runner =
if runner_registration_token_valid?
# Create shared runner. Requires admin access
Ci::Runner.create(attributes.merge(is_shared: true))
elsif project = Project.find_by(runners_token: params[:token])
# Create a specific runner for project.
project.runners.create(attributes)
end
return forbidden! unless runner
if runner.id
runner.update(get_runner_version_from_params)
present runner, with: Entities::RunnerRegistrationDetails
else
not_found!
end
end
desc 'Deletes a registered Runner' do
http_codes [[200, 'Runner was deleted'], [403, 'Forbidden']]
end
params do
requires :token, type: String, desc: %q(Runner's authentication token)
end
delete '/' do
authenticate_runner!
Ci::Runner.find_by_token(params[:token]).destroy
end
end
end
end
...@@ -563,7 +563,20 @@ module API ...@@ -563,7 +563,20 @@ module API
SlackService, SlackService,
MattermostService, MattermostService,
TeamcityService, TeamcityService,
].freeze ]
if Rails.env.development?
services['mock-ci'] = [
{
required: true,
name: :mock_service_url,
type: String,
desc: 'URL to the mock service'
}
]
service_classes << MockCiService
end
trigger_services = { trigger_services = {
'mattermost-slash-commands' => [ 'mattermost-slash-commands' => [
......
...@@ -324,24 +324,30 @@ module Gitlab ...@@ -324,24 +324,30 @@ module Gitlab
end end
def log_by_shell(sha, options) def log_by_shell(sha, options)
cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{path} log) limit = options[:limit].to_i
cmd += %W(-n #{options[:limit].to_i}) offset = options[:offset].to_i
cmd += %w(--format=%H) use_follow_flag = options[:follow] && options[:path].present?
cmd += %W(--skip=#{options[:offset].to_i})
cmd += %w(--follow) if options[:follow] # We will perform the offset in Ruby because --follow doesn't play well with --skip.
cmd += %w(--no-merges) if options[:skip_merges] # See: https://gitlab.com/gitlab-org/gitlab-ce/issues/3574#note_3040520
cmd += %W(--after=#{options[:after].iso8601}) if options[:after] offset_in_ruby = use_follow_flag && options[:offset].present?
cmd += %W(--before=#{options[:before].iso8601}) if options[:before] limit += offset if offset_in_ruby
cmd += [sha]
cmd += %W(-- #{options[:path]}) if options[:path].present? cmd = %W[#{Gitlab.config.git.bin_path} --git-dir=#{path} log]
cmd << "--max-count=#{limit}"
raw_output = IO.popen(cmd) {|io| io.read } cmd << '--format=%H'
cmd << "--skip=#{offset}" unless offset_in_ruby
log = raw_output.lines.map do |c| cmd << '--follow' if use_follow_flag
Rugged::Commit.new(rugged, c.strip) cmd << '--no-merges' if options[:skip_merges]
end cmd << "--after=#{options[:after].iso8601}" if options[:after]
cmd << "--before=#{options[:before].iso8601}" if options[:before]
log.is_a?(Array) ? log : [] cmd << sha
cmd += %W[-- #{options[:path]}] if options[:path].present?
raw_output = IO.popen(cmd) { |io| io.read }
lines = offset_in_ruby ? raw_output.lines.drop(offset) : raw_output.lines
lines.map! { |c| Rugged::Commit.new(rugged, c.strip) }
end end
def sha_from_ref(ref) def sha_from_ref(ref)
......
...@@ -125,14 +125,16 @@ describe Projects::IssuesController do ...@@ -125,14 +125,16 @@ describe Projects::IssuesController do
end end
describe 'PUT #update' do describe 'PUT #update' do
before do
sign_in(user)
project.team << [user, :developer]
end
it_behaves_like 'update invalid issuable', Issue
context 'when moving issue to another private project' do context 'when moving issue to another private project' do
let(:another_project) { create(:empty_project, :private) } let(:another_project) { create(:empty_project, :private) }
before do
sign_in(user)
project.team << [user, :developer]
end
context 'when user has access to move issue' do context 'when user has access to move issue' do
before { another_project.team << [user, :reporter] } before { another_project.team << [user, :reporter] }
......
...@@ -255,6 +255,8 @@ describe Projects::MergeRequestsController do ...@@ -255,6 +255,8 @@ describe Projects::MergeRequestsController do
expect { merge_request.reload.target_branch }.not_to change { merge_request.target_branch } expect { merge_request.reload.target_branch }.not_to change { merge_request.target_branch }
end end
it_behaves_like 'update invalid issuable', MergeRequest
end end
end end
......
...@@ -32,7 +32,7 @@ feature 'Admin disables Git access protocol', feature: true do ...@@ -32,7 +32,7 @@ feature 'Admin disables Git access protocol', feature: true do
scenario 'shows only HTTP url' do scenario 'shows only HTTP url' do
visit_project visit_project
expect(page).to have_content("git clone #{project.http_url_to_repo}") expect(page).to have_content("git clone #{project.http_url_to_repo(admin)}")
expect(page).not_to have_selector('#clone-dropdown') expect(page).not_to have_selector('#clone-dropdown')
end end
end end
......
...@@ -15,7 +15,7 @@ feature 'Profile > SSH Keys', feature: true do ...@@ -15,7 +15,7 @@ feature 'Profile > SSH Keys', feature: true do
scenario 'auto-populates the title', js: true do scenario 'auto-populates the title', js: true do
fill_in('Key', with: attributes_for(:key).fetch(:key)) fill_in('Key', with: attributes_for(:key).fetch(:key))
expect(find_field('Title').value).to eq 'dummy@gitlab.com' expect(page).to have_field("Title", with: "dummy@gitlab.com")
end end
scenario 'saves the new key' do scenario 'saves the new key' do
......
...@@ -56,8 +56,14 @@ feature 'Developer views empty project instructions', feature: true do ...@@ -56,8 +56,14 @@ feature 'Developer views empty project instructions', feature: true do
end end
def expect_instructions_for(protocol) def expect_instructions_for(protocol)
msg = :"#{protocol.downcase}_url_to_repo" url =
case protocol
expect(page).to have_content("git clone #{project.send(msg)}") when 'ssh'
project.ssh_url_to_repo
when 'http'
project.http_url_to_repo(developer)
end
expect(page).to have_content("git clone #{url}")
end end
end end
...@@ -529,7 +529,7 @@ describe Gitlab::Git::Repository, seed_helper: true do ...@@ -529,7 +529,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
commit_with_new_name = nil commit_with_new_name = nil
rename_commit = nil rename_commit = nil
before(:all) do before(:context) do
# Add new commits so that there's a renamed file in the commit history # Add new commits so that there's a renamed file in the commit history
repo = Gitlab::Git::Repository.new(TEST_REPO_PATH).rugged repo = Gitlab::Git::Repository.new(TEST_REPO_PATH).rugged
...@@ -538,49 +538,119 @@ describe Gitlab::Git::Repository, seed_helper: true do ...@@ -538,49 +538,119 @@ describe Gitlab::Git::Repository, seed_helper: true do
commit_with_new_name = new_commit_edit_new_file(repo) commit_with_new_name = new_commit_edit_new_file(repo)
end end
after(:context) do
# Erase our commits so other tests get the original repo
repo = Gitlab::Git::Repository.new(TEST_REPO_PATH).rugged
repo.references.update("refs/heads/master", SeedRepo::LastCommit::ID)
end
context "where 'follow' == true" do context "where 'follow' == true" do
options = { ref: "master", follow: true } let(:options) { { ref: "master", follow: true } }
context "and 'path' is a directory" do context "and 'path' is a directory" do
let(:log_commits) do it "does not follow renames" do
repository.log(options.merge(path: "encoding")) log_commits = repository.log(options.merge(path: "encoding"))
end
it "should not follow renames" do aggregate_failures do
expect(log_commits).to include(commit_with_new_name) expect(log_commits).to include(commit_with_new_name)
expect(log_commits).to include(rename_commit) expect(log_commits).to include(rename_commit)
expect(log_commits).not_to include(commit_with_old_name) expect(log_commits).not_to include(commit_with_old_name)
end
end end
end end
context "and 'path' is a file that matches the new filename" do context "and 'path' is a file that matches the new filename" do
let(:log_commits) do context 'without offset' do
repository.log(options.merge(path: "encoding/CHANGELOG")) it "follows renames" do
log_commits = repository.log(options.merge(path: "encoding/CHANGELOG"))
aggregate_failures do
expect(log_commits).to include(commit_with_new_name)
expect(log_commits).to include(rename_commit)
expect(log_commits).to include(commit_with_old_name)
end
end
end end
it "should follow renames" do context 'with offset=1' do
expect(log_commits).to include(commit_with_new_name) it "follows renames and skip the latest commit" do
expect(log_commits).to include(rename_commit) log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 1))
expect(log_commits).to include(commit_with_old_name)
aggregate_failures do
expect(log_commits).not_to include(commit_with_new_name)
expect(log_commits).to include(rename_commit)
expect(log_commits).to include(commit_with_old_name)
end
end
end
context 'with offset=1', 'and limit=1' do
it "follows renames, skip the latest commit and return only one commit" do
log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 1, limit: 1))
expect(log_commits).to contain_exactly(rename_commit)
end
end
context 'with offset=1', 'and limit=2' do
it "follows renames, skip the latest commit and return only two commits" do
log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 1, limit: 2))
aggregate_failures do
expect(log_commits).to contain_exactly(rename_commit, commit_with_old_name)
end
end
end
context 'with offset=2' do
it "follows renames and skip the latest commit" do
log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 2))
aggregate_failures do
expect(log_commits).not_to include(commit_with_new_name)
expect(log_commits).not_to include(rename_commit)
expect(log_commits).to include(commit_with_old_name)
end
end
end
context 'with offset=2', 'and limit=1' do
it "follows renames, skip the two latest commit and return only one commit" do
log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 2, limit: 1))
expect(log_commits).to contain_exactly(commit_with_old_name)
end
end
context 'with offset=2', 'and limit=2' do
it "follows renames, skip the two latest commit and return only one commit" do
log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 2, limit: 2))
aggregate_failures do
expect(log_commits).not_to include(commit_with_new_name)
expect(log_commits).not_to include(rename_commit)
expect(log_commits).to include(commit_with_old_name)
end
end
end end
end end
context "and 'path' is a file that matches the old filename" do context "and 'path' is a file that matches the old filename" do
let(:log_commits) do it "does not follow renames" do
repository.log(options.merge(path: "CHANGELOG")) log_commits = repository.log(options.merge(path: "CHANGELOG"))
end
it "should not follow renames" do aggregate_failures do
expect(log_commits).to include(commit_with_old_name) expect(log_commits).not_to include(commit_with_new_name)
expect(log_commits).to include(rename_commit) expect(log_commits).to include(rename_commit)
expect(log_commits).not_to include(commit_with_new_name) expect(log_commits).to include(commit_with_old_name)
end
end end
end end
context "unknown ref" do context "unknown ref" do
let(:log_commits) { repository.log(options.merge(ref: 'unknown')) } it "returns an empty array" do
log_commits = repository.log(options.merge(ref: 'unknown'))
it "should return empty" do
expect(log_commits).to eq([]) expect(log_commits).to eq([])
end end
end end
...@@ -699,12 +769,6 @@ describe Gitlab::Git::Repository, seed_helper: true do ...@@ -699,12 +769,6 @@ describe Gitlab::Git::Repository, seed_helper: true do
end end
end end
end end
after(:all) do
# Erase our commits so other tests get the original repo
repo = Gitlab::Git::Repository.new(TEST_REPO_PATH).rugged
repo.references.update("refs/heads/master", SeedRepo::LastCommit::ID)
end
end end
describe "#commits_between" do describe "#commits_between" do
......
...@@ -1894,4 +1894,25 @@ describe Project, models: true do ...@@ -1894,4 +1894,25 @@ describe Project, models: true do
end end
end end
end end
describe '#http_url_to_repo' do
let(:project) { create :empty_project }
context 'when no user is given' do
it 'returns the url to the repo without a username' do
url = project.http_url_to_repo
expect(url).to eq(project.http_url_to_repo)
expect(url).not_to include('@')
end
end
context 'when user is given' do
it 'returns the url to the repo with the username' do
user = build_stubbed(:user)
expect(project.http_url_to_repo(user)).to match(%r{https?:\/\/#{user.username}@})
end
end
end
end end
...@@ -173,11 +173,11 @@ describe API::Members, api: true do ...@@ -173,11 +173,11 @@ describe API::Members, api: true do
expect(response).to have_http_status(400) expect(response).to have_http_status(400)
end end
it 'returns 422 when access_level is not valid' do it 'returns 400 when access_level is not valid' do
post api("/#{source_type.pluralize}/#{source.id}/members", master), post api("/#{source_type.pluralize}/#{source.id}/members", master),
user_id: stranger.id, access_level: 1234 user_id: stranger.id, access_level: 1234
expect(response).to have_http_status(422) expect(response).to have_http_status(400)
end end
end end
end end
...@@ -230,11 +230,11 @@ describe API::Members, api: true do ...@@ -230,11 +230,11 @@ describe API::Members, api: true do
expect(response).to have_http_status(400) expect(response).to have_http_status(400)
end end
it 'returns 422 when access level is not valid' do it 'returns 400 when access level is not valid' do
put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master), put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master),
access_level: 1234 access_level: 1234
expect(response).to have_http_status(422) expect(response).to have_http_status(400)
end end
end end
end end
...@@ -342,7 +342,7 @@ describe API::Members, api: true do ...@@ -342,7 +342,7 @@ describe API::Members, api: true do
post api("/projects/#{project.id}/members", master), post api("/projects/#{project.id}/members", master),
user_id: stranger.id, access_level: Member::OWNER user_id: stranger.id, access_level: Member::OWNER
expect(response).to have_http_status(422) expect(response).to have_http_status(400)
end.to change { project.members.count }.by(0) end.to change { project.members.count }.by(0)
end end
end end
......
require 'spec_helper'
describe API::Runner do
include ApiHelpers
include StubGitlabCalls
let(:registration_token) { 'abcdefg123456' }
before do
stub_gitlab_calls
stub_application_setting(runners_registration_token: registration_token)
end
describe '/api/v4/runners' do
describe 'POST /api/v4/runners' do
context 'when no token is provided' do
it 'returns 400 error' do
post api('/runners')
expect(response).to have_http_status 400
end
end
context 'when invalid token is provided' do
it 'returns 403 error' do
post api('/runners'), token: 'invalid'
expect(response).to have_http_status 403
end
end
context 'when valid token is provided' do
it 'creates runner with default values' do
post api('/runners'), token: registration_token
runner = Ci::Runner.first
expect(response).to have_http_status 201
expect(json_response['id']).to eq(runner.id)
expect(json_response['token']).to eq(runner.token)
expect(runner.run_untagged).to be true
end
context 'when project token is used' do
let(:project) { create(:empty_project) }
it 'creates runner' do
post api('/runners'), token: project.runners_token
expect(response).to have_http_status 201
expect(project.runners.size).to eq(1)
end
end
end
context 'when runner description is provided' do
it 'creates runner' do
post api('/runners'), token: registration_token,
description: 'server.hostname'
expect(response).to have_http_status 201
expect(Ci::Runner.first.description).to eq('server.hostname')
end
end
context 'when runner tags are provided' do
it 'creates runner' do
post api('/runners'), token: registration_token,
tag_list: 'tag1, tag2'
expect(response).to have_http_status 201
expect(Ci::Runner.first.tag_list.sort).to eq(%w(tag1 tag2))
end
end
context 'when option for running untagged jobs is provided' do
context 'when tags are provided' do
it 'creates runner' do
post api('/runners'), token: registration_token,
run_untagged: false,
tag_list: ['tag']
expect(response).to have_http_status 201
expect(Ci::Runner.first.run_untagged).to be false
expect(Ci::Runner.first.tag_list.sort).to eq(['tag'])
end
end
context 'when tags are not provided' do
it 'returns 404 error' do
post api('/runners'), token: registration_token,
run_untagged: false
expect(response).to have_http_status 404
end
end
end
context 'when option for locking Runner is provided' do
it 'creates runner' do
post api('/runners'), token: registration_token,
locked: true
expect(response).to have_http_status 201
expect(Ci::Runner.first.locked).to be true
end
end
%w(name version revision platform architecture).each do |param|
context "when info parameter '#{param}' info is present" do
let(:value) { "#{param}_value" }
it %q(updates provided Runner's parameter) do
post api('/runners'), token: registration_token,
info: { param => value }
expect(response).to have_http_status 201
expect(Ci::Runner.first.read_attribute(param.to_sym)).to eq(value)
end
end
end
end
describe 'DELETE /api/v4/runners' do
context 'when no token is provided' do
it 'returns 400 error' do
delete api('/runners')
expect(response).to have_http_status 400
end
end
context 'when invalid token is provided' do
it 'returns 403 error' do
delete api('/runners'), token: 'invalid'
expect(response).to have_http_status 403
end
end
context 'when valid token is provided' do
let(:runner) { create(:ci_runner) }
it 'deletes Runner' do
delete api('/runners'), token: runner.token
expect(response).to have_http_status 200
expect(Ci::Runner.count).to eq(0)
end
end
end
end
end
...@@ -5,6 +5,7 @@ describe Groups::DestroyService, services: true do ...@@ -5,6 +5,7 @@ describe Groups::DestroyService, services: true do
let!(:user) { create(:user) } let!(:user) { create(:user) }
let!(:group) { create(:group) } let!(:group) { create(:group) }
let!(:nested_group) { create(:group, parent: group) }
let!(:project) { create(:project, namespace: group) } let!(:project) { create(:project, namespace: group) }
let!(:gitlab_shell) { Gitlab::Shell.new } let!(:gitlab_shell) { Gitlab::Shell.new }
let!(:remove_path) { group.path + "+#{group.id}+deleted" } let!(:remove_path) { group.path + "+#{group.id}+deleted" }
...@@ -20,6 +21,7 @@ describe Groups::DestroyService, services: true do ...@@ -20,6 +21,7 @@ describe Groups::DestroyService, services: true do
end end
it { expect(Group.unscoped.all).not_to include(group) } it { expect(Group.unscoped.all).not_to include(group) }
it { expect(Group.unscoped.all).not_to include(nested_group) }
it { expect(Project.unscoped.all).not_to include(project) } it { expect(Project.unscoped.all).not_to include(project) }
end end
......
shared_examples 'update invalid issuable' do |klass|
let(:params) do
{
namespace_id: project.namespace.path,
project_id: project.path,
id: issuable.iid
}
end
let(:issuable) do
klass == Issue ? issue : merge_request
end
before do
if klass == Issue
params.merge!(issue: { title: "any" })
else
params.merge!(merge_request: { title: "any" })
end
end
context 'when updating causes conflicts' do
before do
allow_any_instance_of(issuable.class).to receive(:save).
and_raise(ActiveRecord::StaleObjectError.new(issuable, :save))
end
it 'renders edit when format is html' do
put :update, params
expect(response).to render_template(:edit)
expect(assigns[:conflict]).to be_truthy
end
it 'renders json error message when format is json' do
params.merge!(format: "json")
put :update, params
expect(response.status).to eq(409)
expect(JSON.parse(response.body)).to have_key('errors')
end
end
context 'when updating an invalid issuable' do
before do
key = klass == Issue ? :issue : :merge_request
params[key][:title] = ""
end
it 'renders edit when merge request is invalid' do
put :update, params
expect(response).to render_template(:edit)
end
end
end
...@@ -4414,9 +4414,9 @@ vue-resource@^0.9.3: ...@@ -4414,9 +4414,9 @@ vue-resource@^0.9.3:
version "0.9.3" version "0.9.3"
resolved "https://registry.yarnpkg.com/vue-resource/-/vue-resource-0.9.3.tgz#ab46e1c44ea219142dcc28ae4043b3b04c80959d" resolved "https://registry.yarnpkg.com/vue-resource/-/vue-resource-0.9.3.tgz#ab46e1c44ea219142dcc28ae4043b3b04c80959d"
vue@^2.0.3: vue@^2.1.10:
version "2.0.3" version "2.1.10"
resolved "https://registry.yarnpkg.com/vue/-/vue-2.0.3.tgz#3f7698f83d6ad1f0e35955447901672876c63fde" resolved "https://registry.yarnpkg.com/vue/-/vue-2.1.10.tgz#c9235ca48c7925137be5807832ac4e3ac180427b"
watchpack@^1.2.0: watchpack@^1.2.0:
version "1.2.1" version "1.2.1"
......
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