Merge branch 'master' into 'add-svg-loader'
# Conflicts: # app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6
This commit is contained in:
commit
61f65992a0
2
Gemfile
2
Gemfile
|
@ -20,7 +20,7 @@ gem 'rugged', '~> 0.24.0'
|
|||
# Authentication libraries
|
||||
gem 'devise', '~> 4.2'
|
||||
gem 'doorkeeper', '~> 4.2.0'
|
||||
gem 'omniauth', '~> 1.3.2'
|
||||
gem 'omniauth', '~> 1.4.2'
|
||||
gem 'omniauth-auth0', '~> 1.4.1'
|
||||
gem 'omniauth-azure-oauth2', '~> 0.0.6'
|
||||
gem 'omniauth-cas3', '~> 1.1.2'
|
||||
|
|
|
@ -328,7 +328,7 @@ GEM
|
|||
temple (~> 0.7.6)
|
||||
thor
|
||||
tilt
|
||||
hashie (3.4.4)
|
||||
hashie (3.5.5)
|
||||
health_check (2.2.1)
|
||||
rails (>= 4.0)
|
||||
hipchat (1.5.2)
|
||||
|
@ -441,7 +441,7 @@ GEM
|
|||
octokit (4.6.2)
|
||||
sawyer (~> 0.8.0, >= 0.5.3)
|
||||
oj (2.17.4)
|
||||
omniauth (1.3.2)
|
||||
omniauth (1.4.2)
|
||||
hashie (>= 1.2, < 4)
|
||||
rack (>= 1.0, < 3)
|
||||
omniauth-auth0 (1.4.1)
|
||||
|
@ -920,7 +920,7 @@ DEPENDENCIES
|
|||
oauth2 (~> 1.2.0)
|
||||
octokit (~> 4.6.2)
|
||||
oj (~> 2.17.4)
|
||||
omniauth (~> 1.3.2)
|
||||
omniauth (~> 1.4.2)
|
||||
omniauth-auth0 (~> 1.4.1)
|
||||
omniauth-authentiq (~> 0.3.0)
|
||||
omniauth-azure-oauth2 (~> 0.0.6)
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
/* 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 notes */
|
||||
|
||||
(function() {
|
||||
let $commentButtonTemplate;
|
||||
var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
|
||||
|
||||
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_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_NUMBER_CLASS = 'diff-line-num';
|
||||
|
@ -27,26 +27,29 @@
|
|||
|
||||
TEXT_FILE_SELECTOR = '.text-file';
|
||||
|
||||
DEBOUNCE_TIMEOUT_DURATION = 100;
|
||||
|
||||
function FilesCommentButton(filesContainerElement) {
|
||||
var debounce;
|
||||
this.filesContainerElement = filesContainerElement;
|
||||
this.destroy = bind(this.destroy, this);
|
||||
this.render = bind(this.render, this);
|
||||
this.VIEW_TYPE = $('input#view[type=hidden]').val();
|
||||
debounce = _.debounce(this.render, DEBOUNCE_TIMEOUT_DURATION);
|
||||
$(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);
|
||||
this.hideButton = bind(this.hideButton, this);
|
||||
this.isParallelView = notes.isParallelView();
|
||||
filesContainerElement.on('mouseover', LINE_COLUMN_CLASSES, this.render)
|
||||
.on('mouseleave', LINE_COLUMN_CLASSES, this.hideButton);
|
||||
}
|
||||
|
||||
FilesCommentButton.prototype.render = function(e) {
|
||||
var $currentTarget, buttonParentElement, lineContentElement, textFileElement;
|
||||
var $currentTarget, buttonParentElement, lineContentElement, textFileElement, $button;
|
||||
$currentTarget = $(e.currentTarget);
|
||||
|
||||
buttonParentElement = this.getButtonParent($currentTarget);
|
||||
if (!this.validateButtonParent(buttonParentElement)) return;
|
||||
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);
|
||||
buttonParentElement.append(this.buildButton({
|
||||
|
@ -61,19 +64,16 @@
|
|||
}));
|
||||
};
|
||||
|
||||
FilesCommentButton.prototype.destroy = function(e) {
|
||||
if (this.isMovingToSameType(e)) {
|
||||
return;
|
||||
}
|
||||
$(COMMENT_BUTTON_CLASS, this.getButtonParent($(e.currentTarget))).remove();
|
||||
FilesCommentButton.prototype.hideButton = function(e) {
|
||||
var $currentTarget = $(e.currentTarget);
|
||||
var buttonParentElement = this.getButtonParent($currentTarget);
|
||||
|
||||
buttonParentElement.removeClass('is-over')
|
||||
.nextUntil(`.${LINE_CONTENT_CLASS}`).removeClass('is-over');
|
||||
};
|
||||
|
||||
FilesCommentButton.prototype.buildButton = function(buttonAttributes) {
|
||||
var initializedButtonTemplate;
|
||||
initializedButtonTemplate = COMMENT_BUTTON_TEMPLATE({
|
||||
COMMENT_BUTTON_CLASS: COMMENT_BUTTON_CLASS.substr(1)
|
||||
});
|
||||
return $(initializedButtonTemplate).attr({
|
||||
return $commentButtonTemplate.clone().attr({
|
||||
'data-noteable-type': buttonAttributes.noteableType,
|
||||
'data-noteable-id': buttonAttributes.noteableID,
|
||||
'data-commit-id': buttonAttributes.commitID,
|
||||
|
@ -86,14 +86,14 @@
|
|||
};
|
||||
|
||||
FilesCommentButton.prototype.getTextFileElement = function(hoveredElement) {
|
||||
return $(hoveredElement.closest(TEXT_FILE_SELECTOR));
|
||||
return hoveredElement.closest(TEXT_FILE_SELECTOR);
|
||||
};
|
||||
|
||||
FilesCommentButton.prototype.getLineContent = function(hoveredElement) {
|
||||
if (hoveredElement.hasClass(LINE_CONTENT_CLASS)) {
|
||||
return hoveredElement;
|
||||
}
|
||||
if (this.VIEW_TYPE === 'inline') {
|
||||
if (!this.isParallelView) {
|
||||
return $(hoveredElement).closest(LINE_HOLDER_CLASS).find("." + LINE_CONTENT_CLASS);
|
||||
} else {
|
||||
return $(hoveredElement).next("." + LINE_CONTENT_CLASS);
|
||||
|
@ -101,7 +101,7 @@
|
|||
};
|
||||
|
||||
FilesCommentButton.prototype.getButtonParent = function(hoveredElement) {
|
||||
if (this.VIEW_TYPE === 'inline') {
|
||||
if (!this.isParallelView) {
|
||||
if (hoveredElement.hasClass(OLD_LINE_CLASS)) {
|
||||
return hoveredElement;
|
||||
}
|
||||
|
@ -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) {
|
||||
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) {
|
||||
|
@ -135,6 +126,8 @@
|
|||
})();
|
||||
|
||||
$.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))) {
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -285,5 +285,58 @@
|
|||
* @returns {Boolean}
|
||||
*/
|
||||
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);
|
||||
}).call(window);
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
/**
|
||||
* exports HTTP status codes
|
||||
*/
|
||||
|
||||
const statusCodes = {
|
||||
NO_CONTENT: 204,
|
||||
OK: 200,
|
||||
};
|
||||
|
||||
module.exports = statusCodes;
|
|
@ -84,13 +84,14 @@
|
|||
}
|
||||
|
||||
$(function() {
|
||||
$(document).on('focusout.ssh_key', '#key_key', function() {
|
||||
$(document).on('input.ssh_key', '#key_key', function() {
|
||||
const $title = $('#key_title');
|
||||
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();
|
||||
}
|
||||
// Extract the SSH Key title from its comment
|
||||
});
|
||||
if (global.utils.getPagePath() === 'profiles') {
|
||||
return new Profile();
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* global Flash */
|
||||
require('vendor/task_list');
|
||||
|
||||
class TaskList {
|
||||
|
@ -6,6 +7,16 @@ class TaskList {
|
|||
this.dataType = options.dataType;
|
||||
this.fieldName = options.fieldName;
|
||||
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();
|
||||
}
|
||||
|
||||
|
@ -32,6 +43,7 @@ class TaskList {
|
|||
url: $target.data('update-url') || $('form.js-issuable-update').attr('action'),
|
||||
data: patchData,
|
||||
success: this.onSuccess,
|
||||
error: this.onError,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,17 +39,15 @@ const playIconSvg = require('icons/_icon_play.svg');
|
|||
|
||||
template: `
|
||||
<td class="pipeline-actions hidden-xs">
|
||||
<div class="controls pull-right">
|
||||
<div class="btn-group inline">
|
||||
<div class="pull-right">
|
||||
<div class="btn-group">
|
||||
<div class="btn-group" v-if="actions">
|
||||
<button
|
||||
v-if='actions'
|
||||
class="dropdown-toggle btn btn-default has-tooltip js-pipeline-dropdown-manual-actions"
|
||||
data-toggle="dropdown"
|
||||
title="Manual job"
|
||||
data-placement="top"
|
||||
aria-label="Manual job"
|
||||
>
|
||||
aria-label="Manual job">
|
||||
<span v-html="playIconSvg" aria-hidden="true"></span>
|
||||
<i class="fa fa-caret-down" aria-hidden="true"></i>
|
||||
</button>
|
||||
|
@ -58,23 +56,21 @@ const playIconSvg = require('icons/_icon_play.svg');
|
|||
<a
|
||||
rel="nofollow"
|
||||
data-method="post"
|
||||
:href='action.path'
|
||||
>
|
||||
:href="action.path" >
|
||||
<span v-html="playIconSvg" aria-hidden="true"></span>
|
||||
<span>{{action.name}}</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
|
||||
<div class="btn-group" v-if="artifacts">
|
||||
<button
|
||||
v-if='artifacts'
|
||||
class="dropdown-toggle btn btn-default build-artifacts has-tooltip js-pipeline-dropdown-download"
|
||||
title="Artifacts"
|
||||
data-placement="top"
|
||||
data-toggle="dropdown"
|
||||
aria-label="Artifacts"
|
||||
>
|
||||
aria-label="Artifacts">
|
||||
<i class="fa fa-download" aria-hidden="true"></i>
|
||||
<i class="fa fa-caret-down" aria-hidden="true"></i>
|
||||
</button>
|
||||
|
@ -82,20 +78,16 @@ const playIconSvg = require('icons/_icon_play.svg');
|
|||
<li v-for='artifact in pipeline.details.artifacts'>
|
||||
<a
|
||||
rel="nofollow"
|
||||
download
|
||||
:href='artifact.path'
|
||||
>
|
||||
:href="artifact.path">
|
||||
<i class="fa fa-download" aria-hidden="true"></i>
|
||||
<span>{{download(artifact.name)}}</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cancel-retry-btns inline">
|
||||
<div class="btn-group" v-if="pipeline.flags.retryable">
|
||||
<a
|
||||
v-if='pipeline.flags.retryable'
|
||||
class="btn has-tooltip"
|
||||
class="btn btn-default btn-retry has-tooltip"
|
||||
title="Retry"
|
||||
rel="nofollow"
|
||||
data-method="post"
|
||||
|
@ -105,9 +97,9 @@ const playIconSvg = require('icons/_icon_play.svg');
|
|||
aria-label="Retry">
|
||||
<i class="fa fa-repeat" aria-hidden="true"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="btn-group" v-if="pipeline.flags.cancelable">
|
||||
<a
|
||||
v-if='pipeline.flags.cancelable'
|
||||
@click="confirmAction"
|
||||
class="btn btn-remove has-tooltip"
|
||||
title="Cancel"
|
||||
rel="nofollow"
|
||||
|
@ -120,6 +112,7 @@ const playIconSvg = require('icons/_icon_play.svg');
|
|||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
`,
|
||||
});
|
||||
|
|
|
@ -57,7 +57,7 @@ const iconTimerSvg = require('../../../views/shared/icons/_icon_timer.svg');
|
|||
},
|
||||
},
|
||||
template: `
|
||||
<td>
|
||||
<td class="pipelines-time-ago">
|
||||
<p class="duration" v-if='duration'>
|
||||
<span v-html="iconTimerSvg"></span>
|
||||
{{duration}}
|
||||
|
@ -68,8 +68,7 @@ const iconTimerSvg = require('../../../views/shared/icons/_icon_timer.svg');
|
|||
data-toggle="tooltip"
|
||||
data-placement="top"
|
||||
data-container="body"
|
||||
:data-original-title='localTimeFinished'
|
||||
>
|
||||
:data-original-title='localTimeFinished'>
|
||||
{{timeStopped.words}}
|
||||
</time>
|
||||
</p>
|
||||
|
|
|
@ -229,7 +229,7 @@
|
|||
.controls {
|
||||
float: right;
|
||||
margin-top: 8px;
|
||||
padding-bottom: 7px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid $border-color;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ $dark-highlight-bg: #ffe792;
|
|||
$dark-highlight-color: $black;
|
||||
$dark-pre-hll-bg: #373b41;
|
||||
$dark-hll-bg: #373b41;
|
||||
$dark-over-bg: #9f9ab5;
|
||||
$dark-c: #969896;
|
||||
$dark-err: #c66;
|
||||
$dark-k: #b294bb;
|
||||
|
@ -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 {
|
||||
@include dark-diff-match-line;
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ $monokai-line-empty-bg: #49483e;
|
|||
$monokai-line-empty-border: darken($monokai-line-empty-bg, 15%);
|
||||
$monokai-diff-border: #808080;
|
||||
$monokai-highlight-bg: #ffe792;
|
||||
$monokai-over-bg: #9f9ab5;
|
||||
|
||||
$monokai-new-bg: rgba(166, 226, 46, 0.1);
|
||||
$monokai-new-idiff: rgba(166, 226, 46, 0.15);
|
||||
|
@ -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 {
|
||||
@include dark-diff-match-line;
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ $solarized-dark-line-color-new: #5a766c;
|
|||
$solarized-dark-line-color-old: #7a6c71;
|
||||
$solarized-dark-highlight: #094554;
|
||||
$solarized-dark-hll-bg: #174652;
|
||||
$solarized-dark-over-bg: #9f9ab5;
|
||||
$solarized-dark-c: #586e75;
|
||||
$solarized-dark-err: #93a1a1;
|
||||
$solarized-dark-g: #93a1a1;
|
||||
|
@ -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 {
|
||||
@include dark-diff-match-line;
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ $solarized-light-line-color-new: #a1a080;
|
|||
$solarized-light-line-color-old: #ad9186;
|
||||
$solarized-light-highlight: #eee8d5;
|
||||
$solarized-light-hll-bg: #ddd8c5;
|
||||
$solarized-light-over-bg: #ded7fc;
|
||||
$solarized-light-c: #93a1a1;
|
||||
$solarized-light-err: #586e75;
|
||||
$solarized-light-g: #586e75;
|
||||
|
@ -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 {
|
||||
@include matchLine;
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ $white-code-color: $gl-text-color;
|
|||
$white-highlight: #fafe3d;
|
||||
$white-pre-hll-bg: #f8eec7;
|
||||
$white-hll-bg: #f8f8f8;
|
||||
$white-over-bg: #ded7fc;
|
||||
$white-c: #998;
|
||||
$white-err: #a61717;
|
||||
$white-err-bg: #e3d2d2;
|
||||
|
@ -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) {
|
||||
background-color: $line-number-select;
|
||||
border-color: $line-select-yellow-dark;
|
||||
|
|
|
@ -89,6 +89,10 @@
|
|||
|
||||
.diff-line-num {
|
||||
width: 50px;
|
||||
|
||||
a {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
.line_holder td {
|
||||
|
@ -109,10 +113,6 @@
|
|||
td.line_content.parallel {
|
||||
width: 46%;
|
||||
}
|
||||
|
||||
.add-diff-note {
|
||||
margin-left: -65px;
|
||||
}
|
||||
}
|
||||
|
||||
.old_line,
|
||||
|
|
|
@ -452,36 +452,37 @@ ul.notes {
|
|||
* Line note button on the side of diffs
|
||||
*/
|
||||
|
||||
.diff-file tr.line_holder {
|
||||
@mixin show-add-diff-note {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.add-diff-note {
|
||||
margin-top: -8px;
|
||||
border-radius: 40px;
|
||||
display: none;
|
||||
margin-top: -2px;
|
||||
border-radius: 50%;
|
||||
background: $white-light;
|
||||
padding: 4px;
|
||||
font-size: 16px;
|
||||
padding: 1px 5px;
|
||||
font-size: 12px;
|
||||
color: $gl-link-color;
|
||||
margin-left: -56px;
|
||||
margin-left: -55px;
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
width: 32px;
|
||||
// "hide" it by default
|
||||
display: none;
|
||||
width: 23px;
|
||||
height: 23px;
|
||||
border: 1px solid $border-color;
|
||||
transition: transform .1s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
background: $gl-info;
|
||||
color: $white-light;
|
||||
@include show-add-diff-note;
|
||||
transform: scale(1.15);
|
||||
}
|
||||
|
||||
&:active {
|
||||
outline: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// "show" the icon also if we just hover somewhere over the line
|
||||
&:hover > td {
|
||||
.diff-file {
|
||||
.is-over {
|
||||
.add-diff-note {
|
||||
@include show-add-diff-note;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,21 +13,16 @@
|
|||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.table-holder {
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.commit-title {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.controls {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.btn {
|
||||
margin: 4px;
|
||||
}
|
||||
|
||||
.table.ci-table {
|
||||
min-width: 1200px;
|
||||
table-layout: fixed;
|
||||
|
||||
.label {
|
||||
margin-bottom: 3px;
|
||||
|
@ -37,16 +32,72 @@
|
|||
color: $black;
|
||||
}
|
||||
|
||||
.pipeline-date,
|
||||
.pipeline-status {
|
||||
width: 10%;
|
||||
.stage-cell {
|
||||
min-width: 130px; // Guarantees we show at least 4 stages in line
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
.pipelines-time-ago {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.pipeline-info,
|
||||
.pipeline-commit,
|
||||
.pipeline-stages,
|
||||
.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,28 +112,11 @@
|
|||
}
|
||||
}
|
||||
|
||||
.content-list.pipelines .table-holder {
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
.pipeline-holder {
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.table.ci-table {
|
||||
min-width: 900px;
|
||||
|
||||
&.pipeline {
|
||||
min-width: 650px;
|
||||
}
|
||||
|
||||
&.builds-page {
|
||||
|
||||
tr {
|
||||
&.builds-page tr {
|
||||
height: 71px;
|
||||
}
|
||||
}
|
||||
|
||||
tr {
|
||||
th {
|
||||
|
@ -99,7 +133,7 @@
|
|||
}
|
||||
|
||||
.commit-link {
|
||||
padding: 9px 8px 10px;
|
||||
padding: 9px 8px 10px 2px;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -206,73 +240,9 @@
|
|||
}
|
||||
}
|
||||
|
||||
.pipeline-actions {
|
||||
min-width: 140px;
|
||||
|
||||
.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 {
|
||||
.build-link a {
|
||||
color: $gl-text-color;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-group.open .dropdown-toggle {
|
||||
box-shadow: none;
|
||||
|
@ -335,33 +305,10 @@
|
|||
}
|
||||
|
||||
.tab-pane {
|
||||
&.pipelines {
|
||||
.ci-table {
|
||||
min-width: 900px;
|
||||
}
|
||||
|
||||
.content-list.pipelines {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.stage {
|
||||
max-width: 100px;
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.pipeline-actions {
|
||||
min-width: initial;
|
||||
}
|
||||
}
|
||||
|
||||
&.builds {
|
||||
.ci-table {
|
||||
tr {
|
||||
&.builds .ci-table tr {
|
||||
height: 71px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pipeline graph
|
||||
.pipeline-graph {
|
||||
|
|
|
@ -638,14 +638,6 @@ pre.light-well {
|
|||
margin: 0;
|
||||
}
|
||||
|
||||
.activity-filter-block {
|
||||
.controls {
|
||||
padding-bottom: 7px;
|
||||
margin-top: 8px;
|
||||
border-bottom: 1px solid $border-color;
|
||||
}
|
||||
}
|
||||
|
||||
.commits-search-form {
|
||||
.input-short {
|
||||
min-width: 200px;
|
||||
|
|
|
@ -26,6 +26,23 @@ module IssuableActions
|
|||
|
||||
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
|
||||
@labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute
|
||||
end
|
||||
|
|
|
@ -33,6 +33,7 @@ module ServiceParams
|
|||
:issues_url,
|
||||
:jira_issue_transition_id,
|
||||
:merge_requests_events,
|
||||
:mock_service_url,
|
||||
:namespace,
|
||||
:new_issue_url,
|
||||
:notify,
|
||||
|
|
|
@ -134,8 +134,7 @@ class Projects::IssuesController < Projects::ApplicationController
|
|||
end
|
||||
|
||||
rescue ActiveRecord::StaleObjectError
|
||||
@conflict = true
|
||||
render :edit
|
||||
render_conflict_response
|
||||
end
|
||||
|
||||
def referenced_merge_requests
|
||||
|
|
|
@ -296,22 +296,21 @@ class Projects::MergeRequestsController < Projects::ApplicationController
|
|||
def update
|
||||
@merge_request = MergeRequests::UpdateService.new(project, current_user, merge_request_params).execute(@merge_request)
|
||||
|
||||
if @merge_request.valid?
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
redirect_to([@merge_request.target_project.namespace.becomes(Namespace),
|
||||
@merge_request.target_project, @merge_request])
|
||||
if @merge_request.valid?
|
||||
redirect_to([@merge_request.target_project.namespace.becomes(Namespace), @merge_request.target_project, @merge_request])
|
||||
else
|
||||
render :edit
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
else
|
||||
render "edit"
|
||||
end
|
||||
rescue ActiveRecord::StaleObjectError
|
||||
@conflict = true
|
||||
render :edit
|
||||
render_conflict_response
|
||||
end
|
||||
|
||||
def remove_wip
|
||||
|
|
|
@ -34,7 +34,7 @@ module ButtonHelper
|
|||
|
||||
content_tag (append_link ? :a : :span), protocol,
|
||||
class: klass,
|
||||
href: (project.http_url_to_repo if append_link),
|
||||
href: (project.http_url_to_repo(current_user) if append_link),
|
||||
data: {
|
||||
html: true,
|
||||
placement: placement,
|
||||
|
|
|
@ -241,7 +241,7 @@ module ProjectsHelper
|
|||
when 'ssh'
|
||||
project.ssh_url_to_repo
|
||||
else
|
||||
project.http_url_to_repo
|
||||
project.http_url_to_repo(current_user)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -36,7 +36,7 @@ class Event < ActiveRecord::Base
|
|||
scope :code_push, -> { where(action: PUSHED) }
|
||||
|
||||
scope :in_projects, ->(projects) do
|
||||
where(project_id: projects).recent
|
||||
where(project_id: projects.pluck(:id)).recent
|
||||
end
|
||||
|
||||
scope :with_associations, -> { includes(:author, :project, project: :namespace).preload(:target) }
|
||||
|
|
|
@ -869,8 +869,14 @@ class Project < ActiveRecord::Base
|
|||
url_to_repo
|
||||
end
|
||||
|
||||
def http_url_to_repo
|
||||
"#{web_url}.git"
|
||||
def http_url_to_repo(user = nil)
|
||||
url = web_url
|
||||
|
||||
if user
|
||||
url.sub!(%r{\Ahttps?://}) { |protocol| "#{protocol}#{user.username}@" }
|
||||
end
|
||||
|
||||
"#{url}.git"
|
||||
end
|
||||
|
||||
# Check if current branch name is marked as protected in the system
|
||||
|
|
|
@ -17,8 +17,8 @@ class MattermostService < ChatNotificationService
|
|||
<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#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>Select events below to enable notifications. The channel and username are optional. </li>
|
||||
<li>Paste the webhook <strong>URL</strong> into the field below.</li>
|
||||
<li>Select events below to enable notifications. The <strong>Channel handle</strong> and <strong>Username</strong> fields are optional.</li>
|
||||
</ol>'
|
||||
end
|
||||
|
||||
|
@ -28,14 +28,14 @@ class MattermostService < ChatNotificationService
|
|||
|
||||
def default_fields
|
||||
[
|
||||
{ type: 'text', name: 'webhook', placeholder: 'http://mattermost_host/hooks/...' },
|
||||
{ type: 'text', name: 'username', placeholder: 'username' },
|
||||
{ type: 'text', name: 'webhook', placeholder: 'e.g. http://mattermost_host/hooks/…' },
|
||||
{ type: 'text', name: 'username', placeholder: 'e.g. GitLab' },
|
||||
{ type: 'checkbox', name: 'notify_only_broken_builds' },
|
||||
{ type: 'checkbox', name: 'notify_only_broken_pipelines' },
|
||||
]
|
||||
end
|
||||
|
||||
def default_channel_placeholder
|
||||
"town-square"
|
||||
"Channel handle (e.g. town-square)"
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
# 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
|
|
@ -17,7 +17,7 @@ class SlackService < ChatNotificationService
|
|||
<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>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>'
|
||||
end
|
||||
|
||||
|
@ -27,14 +27,14 @@ class SlackService < ChatNotificationService
|
|||
|
||||
def default_fields
|
||||
[
|
||||
{ type: 'text', name: 'webhook', placeholder: 'https://hooks.slack.com/services/...' },
|
||||
{ type: 'text', name: 'username', placeholder: 'username' },
|
||||
{ type: 'text', name: 'webhook', placeholder: 'e.g. https://hooks.slack.com/services/…' },
|
||||
{ type: 'text', name: 'username', placeholder: 'e.g. GitLab' },
|
||||
{ type: 'checkbox', name: 'notify_only_broken_builds' },
|
||||
{ type: 'checkbox', name: 'notify_only_broken_pipelines' },
|
||||
]
|
||||
end
|
||||
|
||||
def default_channel_placeholder
|
||||
"#general"
|
||||
"Channel name (e.g. general)"
|
||||
end
|
||||
end
|
||||
|
|
|
@ -109,9 +109,7 @@ class Repository
|
|||
offset: offset,
|
||||
after: after,
|
||||
before: before,
|
||||
# --follow doesn't play well with --skip. See:
|
||||
# https://gitlab.com/gitlab-org/gitlab-ce/issues/3574#note_3040520
|
||||
follow: false,
|
||||
follow: path.present?,
|
||||
skip_merges: skip_merges
|
||||
}
|
||||
|
||||
|
|
|
@ -210,7 +210,7 @@ class Service < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def self.available_services_names
|
||||
%w[
|
||||
service_names = %w[
|
||||
asana
|
||||
assembla
|
||||
bamboo
|
||||
|
@ -238,6 +238,9 @@ class Service < ActiveRecord::Base
|
|||
slack
|
||||
teamcity
|
||||
]
|
||||
service_names << 'mock_ci' if Rails.env.development?
|
||||
|
||||
service_names.sort_by(&:downcase)
|
||||
end
|
||||
|
||||
def self.build_from_template(project_id, template)
|
||||
|
|
|
@ -18,7 +18,8 @@ module Groups
|
|||
end
|
||||
|
||||
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
|
||||
|
||||
group.really_destroy!
|
||||
|
|
|
@ -14,6 +14,8 @@
|
|||
= runner.short_sha
|
||||
%td
|
||||
= runner.description
|
||||
%td
|
||||
= runner.version
|
||||
%td
|
||||
- if runner.shared?
|
||||
n/a
|
||||
|
|
|
@ -67,6 +67,7 @@
|
|||
%th Type
|
||||
%th Runner token
|
||||
%th Description
|
||||
%th Version
|
||||
%th Projects
|
||||
%th Jobs
|
||||
%th Tags
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
.nav-block
|
||||
- if current_user
|
||||
.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
|
||||
= render 'shared/event_filter'
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
.nav-block
|
||||
- if current_user
|
||||
.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
|
||||
= render 'shared/event_filter'
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
.nav-block.activity-filter-block
|
||||
- if current_user
|
||||
.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')
|
||||
|
||||
= render 'shared/event_filter'
|
||||
|
|
|
@ -1,92 +0,0 @@
|
|||
- 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")
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Add runner version to /admin/runners view
|
||||
merge_request: 8733
|
||||
author: Jonathon Reinhart
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Add the Username to the HTTP(S) clone URL of a Repository
|
||||
merge_request: 9347
|
||||
author: Jan Christophersen
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Make Git history follow renames again by performing the --skip in Ruby
|
||||
merge_request:
|
||||
author:
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: 'Add performance query regression fix for !9088 affecting #27267'
|
||||
merge_request:
|
||||
author:
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Add Runner's registration/deletion v4 API
|
||||
merge_request: 9246
|
||||
author:
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Fix issuable stale object error handler for js when updating tasklists
|
||||
merge_request:
|
||||
author:
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Add Mock CI service/integration for development
|
||||
merge_request:
|
||||
author:
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Improved diff comment button UX
|
||||
merge_request:
|
||||
author:
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Fixed RSS button alignment on activity pages
|
||||
merge_request:
|
||||
author:
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Bump Hashie to 3.5.5 and omniauth to 1.4.2 to eliminate warning noise
|
||||
merge_request:
|
||||
author:
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: SSH key field updates title after pasting key
|
||||
merge_request:
|
||||
author:
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: 'API: Return 400 for all validation erros in the mebers API'
|
||||
merge_request: 9523
|
||||
author: Robert Schilling
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: update Vue to v2.1.10
|
||||
merge_request: 9386
|
||||
author:
|
|
@ -92,7 +92,7 @@ var config = {
|
|||
'emoji-aliases$': path.join(ROOT_PATH, 'fixtures/emojis/aliases.json'),
|
||||
'icons': path.join(ROOT_PATH, 'app/views/shared/icons'),
|
||||
'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
|
|||
project, you can disable it from your project's settings. Read the user guide
|
||||
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
|
||||
|
||||
Currently, there is no storage limitation, which means a user can upload an
|
||||
|
|
|
@ -810,3 +810,38 @@ GET /projects/:id/services/teamcity
|
|||
|
||||
[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
|
||||
|
||||
|
||||
## 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:
|
|||
- 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)
|
||||
- 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)
|
||||
- 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)
|
|
@ -1018,7 +1018,7 @@ A simple example:
|
|||
|
||||
```yaml
|
||||
job1:
|
||||
coverage: /Code coverage: \d+\.\d+/
|
||||
coverage: '/Code coverage: \d+\.\d+/'
|
||||
```
|
||||
|
||||
## Git Strategy
|
||||
|
|
|
@ -2,11 +2,12 @@
|
|||
|
||||
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)
|
||||
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)
|
||||
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 |
|
||||
|---------------------------------------|---------------------------|---------------------------------------------------------------------------|-----------|
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 244 KiB |
Binary file not shown.
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 224 KiB |
|
@ -24,23 +24,24 @@ There, you will see a checkbox with the following events that can be triggered:
|
|||
|
||||
- Push
|
||||
- Issue
|
||||
- Confidential issue
|
||||
- Merge request
|
||||
- Note
|
||||
- Tag push
|
||||
- Build
|
||||
- Pipeline
|
||||
- Wiki page
|
||||
|
||||
Bellow each of these event checkboxes, you will have an input field to insert
|
||||
which Mattermost channel you want to send that event message, with `#town-square`
|
||||
being the default. The hash sign is optional.
|
||||
Below each of these event checkboxes, you have an input field to enter
|
||||
which Mattermost channel you want to send that event message. Enter your preferred channel handle (the hash sign `#` is optional).
|
||||
|
||||
At the end, fill in your Mattermost details:
|
||||
|
||||
| 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. |
|
||||
| **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)
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
# 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:
|
|||
|
||||
- Push
|
||||
- Issue
|
||||
- Confidential issue
|
||||
- Merge request
|
||||
- Note
|
||||
- Tag push
|
||||
- Build
|
||||
- Pipeline
|
||||
- Wiki page
|
||||
|
||||
Bellow each of these event checkboxes, you will have an input field to insert
|
||||
which Slack channel you want to send that event message, with `#general`
|
||||
being the default. Enter your preferred channel **without** the hash sign (`#`).
|
||||
Below each of these event checkboxes, you have an input field to enter
|
||||
which Slack channel you want to send that event message. Enter your preferred channel name **without** the hash sign (`#`).
|
||||
|
||||
At the end, fill in your Slack details:
|
||||
|
||||
| Field | Description |
|
||||
| ----- | ----------- |
|
||||
| **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 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.
|
||||
|
||||
|
|
|
@ -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
|
||||
```
|
||||
|
||||
>**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
|
||||
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
|
||||
|
|
|
@ -49,7 +49,7 @@ class Spinach::Features::ExploreProjects < Spinach::FeatureSteps
|
|||
|
||||
step 'I should see an http link to the repository' do
|
||||
project = Project.find_by(name: 'Community')
|
||||
expect(page).to have_field('project_clone', with: project.http_url_to_repo)
|
||||
expect(page).to have_field('project_clone', with: project.http_url_to_repo(@user))
|
||||
end
|
||||
|
||||
step 'I should see an ssh link to the repository' do
|
||||
|
|
|
@ -91,6 +91,7 @@ module API
|
|||
mount ::API::Projects
|
||||
mount ::API::ProjectSnippets
|
||||
mount ::API::Repositories
|
||||
mount ::API::Runner
|
||||
mount ::API::Runners
|
||||
mount ::API::Services
|
||||
mount ::API::Session
|
||||
|
|
|
@ -618,6 +618,10 @@ module API
|
|||
end
|
||||
end
|
||||
|
||||
class RunnerRegistrationDetails < Grape::Entity
|
||||
expose :id, :token
|
||||
end
|
||||
|
||||
class BuildArtifactFile < Grape::Entity
|
||||
expose :filename, :size
|
||||
end
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
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
|
|||
authorize_admin_source!(source_type, source)
|
||||
|
||||
member = source.members.find_by(user_id: params[:user_id])
|
||||
|
||||
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])
|
||||
|
@ -63,9 +62,6 @@ module API
|
|||
if member.persisted? && member.valid?
|
||||
present member.user, with: Entities::Member, member: member
|
||||
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)
|
||||
end
|
||||
end
|
||||
|
@ -87,9 +83,6 @@ module API
|
|||
if member.update_attributes(declared_params(include_missing: false))
|
||||
present member.user, with: Entities::Member, member: member
|
||||
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)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
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
|
|||
SlackService,
|
||||
MattermostService,
|
||||
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 = {
|
||||
'mattermost-slash-commands' => [
|
||||
|
|
|
@ -324,24 +324,30 @@ module Gitlab
|
|||
end
|
||||
|
||||
def log_by_shell(sha, options)
|
||||
cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{path} log)
|
||||
cmd += %W(-n #{options[:limit].to_i})
|
||||
cmd += %w(--format=%H)
|
||||
cmd += %W(--skip=#{options[:offset].to_i})
|
||||
cmd += %w(--follow) if options[:follow]
|
||||
cmd += %w(--no-merges) if options[:skip_merges]
|
||||
cmd += %W(--after=#{options[:after].iso8601}) if options[:after]
|
||||
cmd += %W(--before=#{options[:before].iso8601}) if options[:before]
|
||||
cmd += [sha]
|
||||
cmd += %W(-- #{options[:path]}) if options[:path].present?
|
||||
limit = options[:limit].to_i
|
||||
offset = options[:offset].to_i
|
||||
use_follow_flag = options[:follow] && options[:path].present?
|
||||
|
||||
# We will perform the offset in Ruby because --follow doesn't play well with --skip.
|
||||
# See: https://gitlab.com/gitlab-org/gitlab-ce/issues/3574#note_3040520
|
||||
offset_in_ruby = use_follow_flag && options[:offset].present?
|
||||
limit += offset if offset_in_ruby
|
||||
|
||||
cmd = %W[#{Gitlab.config.git.bin_path} --git-dir=#{path} log]
|
||||
cmd << "--max-count=#{limit}"
|
||||
cmd << '--format=%H'
|
||||
cmd << "--skip=#{offset}" unless offset_in_ruby
|
||||
cmd << '--follow' if use_follow_flag
|
||||
cmd << '--no-merges' if options[:skip_merges]
|
||||
cmd << "--after=#{options[:after].iso8601}" if options[:after]
|
||||
cmd << "--before=#{options[:before].iso8601}" if options[:before]
|
||||
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
|
||||
|
||||
log = raw_output.lines.map do |c|
|
||||
Rugged::Commit.new(rugged, c.strip)
|
||||
end
|
||||
|
||||
log.is_a?(Array) ? log : []
|
||||
lines.map! { |c| Rugged::Commit.new(rugged, c.strip) }
|
||||
end
|
||||
|
||||
def sha_from_ref(ref)
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
"stats-webpack-plugin": "^0.4.3",
|
||||
"timeago.js": "^2.0.5",
|
||||
"underscore": "^1.8.3",
|
||||
"vue": "^2.0.3",
|
||||
"vue": "^2.1.10",
|
||||
"vue-resource": "^0.9.3",
|
||||
"webpack": "^2.2.1",
|
||||
"webpack-bundle-analyzer": "^2.3.0"
|
||||
|
|
|
@ -125,14 +125,16 @@ describe Projects::IssuesController do
|
|||
end
|
||||
|
||||
describe 'PUT #update' do
|
||||
context 'when moving issue to another private project' do
|
||||
let(:another_project) { create(:empty_project, :private) }
|
||||
|
||||
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
|
||||
let(:another_project) { create(:empty_project, :private) }
|
||||
|
||||
context 'when user has access to move issue' do
|
||||
before { another_project.team << [user, :reporter] }
|
||||
|
||||
|
|
|
@ -255,6 +255,8 @@ describe Projects::MergeRequestsController do
|
|||
|
||||
expect { merge_request.reload.target_branch }.not_to change { merge_request.target_branch }
|
||||
end
|
||||
|
||||
it_behaves_like 'update invalid issuable', MergeRequest
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -32,7 +32,7 @@ feature 'Admin disables Git access protocol', feature: true do
|
|||
scenario 'shows only HTTP url' do
|
||||
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')
|
||||
end
|
||||
end
|
||||
|
|
|
@ -15,7 +15,7 @@ feature 'Profile > SSH Keys', feature: true do
|
|||
scenario 'auto-populates the title', js: true do
|
||||
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
|
||||
|
||||
scenario 'saves the new key' do
|
||||
|
|
|
@ -56,8 +56,14 @@ feature 'Developer views empty project instructions', feature: true do
|
|||
end
|
||||
|
||||
def expect_instructions_for(protocol)
|
||||
msg = :"#{protocol.downcase}_url_to_repo"
|
||||
url =
|
||||
case protocol
|
||||
when 'ssh'
|
||||
project.ssh_url_to_repo
|
||||
when 'http'
|
||||
project.http_url_to_repo(developer)
|
||||
end
|
||||
|
||||
expect(page).to have_content("git clone #{project.send(msg)}")
|
||||
expect(page).to have_content("git clone #{url}")
|
||||
end
|
||||
end
|
||||
|
|
|
@ -529,7 +529,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
|
|||
commit_with_new_name = nil
|
||||
rename_commit = nil
|
||||
|
||||
before(:all) do
|
||||
before(:context) do
|
||||
# Add new commits so that there's a renamed file in the commit history
|
||||
repo = Gitlab::Git::Repository.new(TEST_REPO_PATH).rugged
|
||||
|
||||
|
@ -538,49 +538,119 @@ describe Gitlab::Git::Repository, seed_helper: true do
|
|||
commit_with_new_name = new_commit_edit_new_file(repo)
|
||||
end
|
||||
|
||||
context "where 'follow' == true" do
|
||||
options = { ref: "master", follow: true }
|
||||
|
||||
context "and 'path' is a directory" do
|
||||
let(:log_commits) do
|
||||
repository.log(options.merge(path: "encoding"))
|
||||
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
|
||||
|
||||
it "should not follow renames" do
|
||||
context "where 'follow' == true" do
|
||||
let(:options) { { ref: "master", follow: true } }
|
||||
|
||||
context "and 'path' is a directory" do
|
||||
it "does not follow renames" do
|
||||
log_commits = repository.log(options.merge(path: "encoding"))
|
||||
|
||||
aggregate_failures do
|
||||
expect(log_commits).to include(commit_with_new_name)
|
||||
expect(log_commits).to include(rename_commit)
|
||||
expect(log_commits).not_to include(commit_with_old_name)
|
||||
end
|
||||
end
|
||||
|
||||
context "and 'path' is a file that matches the new filename" do
|
||||
let(:log_commits) do
|
||||
repository.log(options.merge(path: "encoding/CHANGELOG"))
|
||||
end
|
||||
|
||||
it "should follow renames" do
|
||||
context "and 'path' is a file that matches the new filename" do
|
||||
context 'without offset' do
|
||||
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
|
||||
|
||||
context "and 'path' is a file that matches the old filename" do
|
||||
let(:log_commits) do
|
||||
repository.log(options.merge(path: "CHANGELOG"))
|
||||
end
|
||||
|
||||
it "should not follow renames" do
|
||||
expect(log_commits).to include(commit_with_old_name)
|
||||
expect(log_commits).to include(rename_commit)
|
||||
context 'with offset=1' do
|
||||
it "follows renames and skip the latest commit" do
|
||||
log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 1))
|
||||
|
||||
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
|
||||
|
||||
context "and 'path' is a file that matches the old filename" do
|
||||
it "does not follow renames" do
|
||||
log_commits = repository.log(options.merge(path: "CHANGELOG"))
|
||||
|
||||
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 "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([])
|
||||
end
|
||||
end
|
||||
|
@ -699,12 +769,6 @@ describe Gitlab::Git::Repository, seed_helper: true do
|
|||
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
|
||||
|
||||
describe "#commits_between" do
|
||||
|
|
|
@ -1894,4 +1894,25 @@ describe Project, models: true do
|
|||
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
|
||||
|
|
|
@ -173,11 +173,11 @@ describe API::Members, api: true do
|
|||
expect(response).to have_http_status(400)
|
||||
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),
|
||||
user_id: stranger.id, access_level: 1234
|
||||
|
||||
expect(response).to have_http_status(422)
|
||||
expect(response).to have_http_status(400)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -230,11 +230,11 @@ describe API::Members, api: true do
|
|||
expect(response).to have_http_status(400)
|
||||
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),
|
||||
access_level: 1234
|
||||
|
||||
expect(response).to have_http_status(422)
|
||||
expect(response).to have_http_status(400)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -342,7 +342,7 @@ describe API::Members, api: true do
|
|||
post api("/projects/#{project.id}/members", master),
|
||||
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
|
||||
end
|
||||
|
|
|
@ -0,0 +1,148 @@
|
|||
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
|
|||
|
||||
let!(:user) { create(:user) }
|
||||
let!(:group) { create(:group) }
|
||||
let!(:nested_group) { create(:group, parent: group) }
|
||||
let!(:project) { create(:project, namespace: group) }
|
||||
let!(:gitlab_shell) { Gitlab::Shell.new }
|
||||
let!(:remove_path) { group.path + "+#{group.id}+deleted" }
|
||||
|
@ -20,6 +21,7 @@ describe Groups::DestroyService, services: true do
|
|||
end
|
||||
|
||||
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) }
|
||||
end
|
||||
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
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:
|
|||
version "0.9.3"
|
||||
resolved "https://registry.yarnpkg.com/vue-resource/-/vue-resource-0.9.3.tgz#ab46e1c44ea219142dcc28ae4043b3b04c80959d"
|
||||
|
||||
vue@^2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/vue/-/vue-2.0.3.tgz#3f7698f83d6ad1f0e35955447901672876c63fde"
|
||||
vue@^2.1.10:
|
||||
version "2.1.10"
|
||||
resolved "https://registry.yarnpkg.com/vue/-/vue-2.1.10.tgz#c9235ca48c7925137be5807832ac4e3ac180427b"
|
||||
|
||||
watchpack@^1.2.0:
|
||||
version "1.2.1"
|
||||
|
|
Loading…
Reference in New Issue