summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFilipa Lacerda <lacerda.filipa@gmail.com>2017-02-27 17:10:25 +0000
committerFilipa Lacerda <lacerda.filipa@gmail.com>2017-02-27 17:10:25 +0000
commit61f65992a05e8fc5ab756ac0ce890d090c6cf4eb (patch)
tree57952a5fa69f757bcd33247e6e3642ba98c6252b
parent44622abe9603c5419bc50212654f737343012ca8 (diff)
parent883342ce36b5f38156a3376af7d8a2e274163917 (diff)
downloadgitlab-ce-61f65992a05e8fc5ab756ac0ce890d090c6cf4eb.tar.gz
Merge branch 'master' into 'add-svg-loader'
# Conflicts: # app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.lock6
-rw-r--r--app/assets/javascripts/files_comment_button.js71
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js.es653
-rw-r--r--app/assets/javascripts/lib/utils/http_status.js10
-rw-r--r--app/assets/javascripts/profile/profile.js.es67
-rw-r--r--app/assets/javascripts/task_list.js12
-rw-r--r--app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es677
-rw-r--r--app/assets/javascripts/vue_pipelines_index/time_ago.js.es65
-rw-r--r--app/assets/stylesheets/framework/blocks.scss2
-rw-r--r--app/assets/stylesheets/highlight/dark.scss13
-rw-r--r--app/assets/stylesheets/highlight/monokai.scss13
-rw-r--r--app/assets/stylesheets/highlight/solarized_dark.scss13
-rw-r--r--app/assets/stylesheets/highlight/solarized_light.scss13
-rw-r--r--app/assets/stylesheets/highlight/white.scss11
-rw-r--r--app/assets/stylesheets/pages/diff.scss8
-rw-r--r--app/assets/stylesheets/pages/notes.scss51
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss203
-rw-r--r--app/assets/stylesheets/pages/projects.scss8
-rw-r--r--app/controllers/concerns/issuable_actions.rb17
-rw-r--r--app/controllers/concerns/service_params.rb1
-rw-r--r--app/controllers/projects/issues_controller.rb3
-rw-r--r--app/controllers/projects/merge_requests_controller.rb23
-rw-r--r--app/helpers/button_helper.rb2
-rw-r--r--app/helpers/projects_helper.rb2
-rw-r--r--app/models/event.rb2
-rw-r--r--app/models/project.rb10
-rw-r--r--app/models/project_services/mattermost_service.rb14
-rw-r--r--app/models/project_services/mock_ci_service.rb82
-rw-r--r--app/models/project_services/slack_service.rb14
-rw-r--r--app/models/repository.rb4
-rw-r--r--app/models/service.rb5
-rw-r--r--app/services/groups/destroy_service.rb3
-rw-r--r--app/views/admin/runners/_runner.html.haml2
-rw-r--r--app/views/admin/runners/index.html.haml1
-rw-r--r--app/views/dashboard/_activities.html.haml2
-rw-r--r--app/views/groups/_activities.html.haml2
-rw-r--r--app/views/projects/_activity.html.haml2
-rw-r--r--app/views/projects/ci/pipelines/_pipeline.html.haml92
-rw-r--r--changelogs/unreleased/14748-runner-version-in-admin-views.yml4
-rw-r--r--changelogs/unreleased/1937-https-clone-url-username.yml4
-rw-r--r--changelogs/unreleased/23062-allow-git-log-to-accept-follow-and-skip.yml4
-rw-r--r--changelogs/unreleased/27267-events-project-query-performance-regression.yml4
-rw-r--r--changelogs/unreleased/feature-runners-registration-deletion-v4-api.yml4
-rw-r--r--changelogs/unreleased/issue_24815.yml4
-rw-r--r--changelogs/unreleased/mock-ci-service.yml4
-rw-r--r--changelogs/unreleased/mr-diff-comment-button.yml4
-rw-r--r--changelogs/unreleased/rss-btn-alignment-fix.yml4
-rw-r--r--changelogs/unreleased/sh-bump-hashie-to-3-5-5.yml4
-rw-r--r--changelogs/unreleased/ssh-key-paste.yml4
-rw-r--r--changelogs/unreleased/unified-member-api-response.yml4
-rw-r--r--changelogs/unreleased/update-vue-2-1.yml4
-rw-r--r--config/webpack.config.js2
-rw-r--r--doc/administration/container_registry.md40
-rw-r--r--doc/api/services.md35
-rw-r--r--doc/api/v3_to_v4.md3
-rw-r--r--doc/ci/yaml/README.md2
-rw-r--r--doc/development/ci_setup.md3
-rw-r--r--doc/user/project/integrations/img/mattermost_configuration.pngbin73502 -> 249592 bytes
-rw-r--r--doc/user/project/integrations/img/slack_configuration.pngbin29825 -> 229050 bytes
-rw-r--r--doc/user/project/integrations/mattermost.md11
-rw-r--r--doc/user/project/integrations/mock_ci.md13
-rw-r--r--doc/user/project/integrations/slack.md10
-rw-r--r--doc/workflow/lfs/manage_large_binaries_with_git_lfs.md6
-rw-r--r--features/steps/explore/projects.rb2
-rw-r--r--lib/api/api.rb1
-rw-r--r--lib/api/entities.rb4
-rw-r--r--lib/api/helpers/runner.rb23
-rw-r--r--lib/api/members.rb7
-rw-r--r--lib/api/runner.rb52
-rw-r--r--lib/api/services.rb15
-rw-r--r--lib/gitlab/git/repository.rb42
-rw-r--r--package.json2
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb12
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb2
-rw-r--r--spec/features/admin/admin_disables_git_access_protocol_spec.rb2
-rw-r--r--spec/features/profiles/keys_spec.rb2
-rw-r--r--spec/features/projects/developer_views_empty_project_instructions_spec.rb12
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb124
-rw-r--r--spec/models/project_spec.rb21
-rw-r--r--spec/requests/api/members_spec.rb10
-rw-r--r--spec/requests/api/runner_spec.rb148
-rw-r--r--spec/services/groups/destroy_service_spec.rb2
-rw-r--r--spec/support/update_invalid_issuable.rb57
-rw-r--r--yarn.lock6
85 files changed, 1105 insertions, 479 deletions
diff --git a/Gemfile b/Gemfile
index 8715acba24f..a3d036792e8 100644
--- a/Gemfile
+++ b/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'
diff --git a/Gemfile.lock b/Gemfile.lock
index b46b09c7544..5ff18442c4f 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -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)
diff --git a/app/assets/javascripts/files_comment_button.js b/app/assets/javascripts/files_comment_button.js
index 698870d0ce1..6d86888dcb8 100644
--- a/app/assets/javascripts/files_comment_button.js
+++ b/app/assets/javascripts/files_comment_button.js
@@ -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;
}
diff --git a/app/assets/javascripts/lib/utils/common_utils.js.es6 b/app/assets/javascripts/lib/utils/common_utils.js.es6
index 643c28dd046..a1423b6fda5 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js.es6
+++ b/app/assets/javascripts/lib/utils/common_utils.js.es6
@@ -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);
diff --git a/app/assets/javascripts/lib/utils/http_status.js b/app/assets/javascripts/lib/utils/http_status.js
new file mode 100644
index 00000000000..bc109a69c20
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/http_status.js
@@ -0,0 +1,10 @@
+/**
+ * exports HTTP status codes
+ */
+
+const statusCodes = {
+ NO_CONTENT: 204,
+ OK: 200,
+};
+
+module.exports = statusCodes;
diff --git a/app/assets/javascripts/profile/profile.js.es6 b/app/assets/javascripts/profile/profile.js.es6
index 81374296522..4ccea0624ee 100644
--- a/app/assets/javascripts/profile/profile.js.es6
+++ b/app/assets/javascripts/profile/profile.js.es6
@@ -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();
diff --git a/app/assets/javascripts/task_list.js b/app/assets/javascripts/task_list.js
index dfe24d1fb33..b1402c0a880 100644
--- a/app/assets/javascripts/task_list.js
+++ b/app/assets/javascripts/task_list.js
@@ -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,
});
}
}
diff --git a/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6
index 1ffb1543f71..f643213ee54 100644
--- a/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6
+++ b/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6
@@ -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="btn-group">
+ <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,42 +78,39 @@ 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">
- <a
- v-if='pipeline.flags.retryable'
- class="btn has-tooltip"
- title="Retry"
- rel="nofollow"
- data-method="post"
- data-placement="top"
- data-toggle="dropdown"
- :href='pipeline.retry_path'
- aria-label="Retry">
- <i class="fa fa-repeat" aria-hidden="true"></i>
- </a>
- <a
- v-if='pipeline.flags.cancelable'
- @click="confirmAction"
- class="btn btn-remove has-tooltip"
- title="Cancel"
- rel="nofollow"
- data-method="post"
- data-placement="top"
- data-toggle="dropdown"
- :href='pipeline.cancel_path'
- aria-label="Cancel">
- <i class="fa fa-remove" aria-hidden="true"></i>
- </a>
+ <div class="btn-group" v-if="pipeline.flags.retryable">
+ <a
+ class="btn btn-default btn-retry has-tooltip"
+ title="Retry"
+ rel="nofollow"
+ data-method="post"
+ data-placement="top"
+ data-toggle="dropdown"
+ :href='pipeline.retry_path'
+ aria-label="Retry">
+ <i class="fa fa-repeat" aria-hidden="true"></i>
+ </a>
+ </div>
+ <div class="btn-group" v-if="pipeline.flags.cancelable">
+ <a
+ class="btn btn-remove has-tooltip"
+ title="Cancel"
+ rel="nofollow"
+ data-method="post"
+ data-placement="top"
+ data-toggle="dropdown"
+ :href='pipeline.cancel_path'
+ aria-label="Cancel">
+ <i class="fa fa-remove" aria-hidden="true"></i>
+ </a>
+ </div>
</div>
</div>
</td>
diff --git a/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6 b/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6
index 22f1b1a8483..a383570857d 100644
--- a/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6
+++ b/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6
@@ -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>
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index 0f9213b98e3..9a4129cdc8d 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -229,7 +229,7 @@
.controls {
float: right;
margin-top: 8px;
- padding-bottom: 7px;
+ padding-bottom: 8px;
border-bottom: 1px solid $border-color;
}
}
diff --git a/app/assets/stylesheets/highlight/dark.scss b/app/assets/stylesheets/highlight/dark.scss
index 6f2e746d4b0..9c76e58dfc8 100644
--- a/app/assets/stylesheets/highlight/dark.scss
+++ b/app/assets/stylesheets/highlight/dark.scss
@@ -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;
}
diff --git a/app/assets/stylesheets/highlight/monokai.scss b/app/assets/stylesheets/highlight/monokai.scss
index 2144a5f7466..6488a099c74 100644
--- a/app/assets/stylesheets/highlight/monokai.scss
+++ b/app/assets/stylesheets/highlight/monokai.scss
@@ -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;
}
diff --git a/app/assets/stylesheets/highlight/solarized_dark.scss b/app/assets/stylesheets/highlight/solarized_dark.scss
index 2cb1d18f12f..00079cc2b5b 100644
--- a/app/assets/stylesheets/highlight/solarized_dark.scss
+++ b/app/assets/stylesheets/highlight/solarized_dark.scss
@@ -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;
}
diff --git a/app/assets/stylesheets/highlight/solarized_light.scss b/app/assets/stylesheets/highlight/solarized_light.scss
index b72c4326730..2e3b68f1a58 100644
--- a/app/assets/stylesheets/highlight/solarized_light.scss
+++ b/app/assets/stylesheets/highlight/solarized_light.scss
@@ -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;
}
diff --git a/app/assets/stylesheets/highlight/white.scss b/app/assets/stylesheets/highlight/white.scss
index 398fbfd3b18..0eca30e570f 100644
--- a/app/assets/stylesheets/highlight/white.scss
+++ b/app/assets/stylesheets/highlight/white.scss
@@ -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;
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index 92d7772da57..339cdcde480 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -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,
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index aa130a1abb0..00f5f2645b3 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -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 {
+ display: none;
+ 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 {
- margin-top: -8px;
- border-radius: 40px;
- background: $white-light;
- 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 {
+ background: $gl-info;
+ color: $white-light;
+ transform: scale(1.15);
+ }
- &:hover {
- background: $gl-info;
- color: $white-light;
- @include show-add-diff-note;
- }
+ &: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;
}
}
}
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 3fe1eef307e..08b3206f31e 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -13,21 +13,16 @@
white-space: nowrap;
}
- .commit-title {
- margin: 0;
- }
-
- .controls {
- white-space: nowrap;
+ .table-holder {
+ width: 100%;
+ overflow: auto;
}
- .btn {
- margin: 4px;
+ .commit-title {
+ margin: 0;
}
.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,27 +112,10 @@
}
}
-.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 {
- height: 71px;
- }
+ &.builds-page tr {
+ height: 71px;
}
tr {
@@ -99,7 +133,7 @@
}
.commit-link {
- padding: 9px 8px 10px;
+ padding: 9px 8px 10px 2px;
}
}
@@ -206,72 +240,8 @@
}
}
- .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 {
- color: $gl-text-color;
- }
+ .build-link a {
+ color: $gl-text-color;
}
.btn-group.open .dropdown-toggle {
@@ -335,31 +305,8 @@
}
.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 {
- height: 71px;
- }
- }
+ &.builds .ci-table tr {
+ height: 71px;
}
}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 67110813abb..07b93430442 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -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;
diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb
index 0821974aa93..3ccf2a9ce33 100644
--- a/app/controllers/concerns/issuable_actions.rb
+++ b/app/controllers/concerns/issuable_actions.rb
@@ -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
diff --git a/app/controllers/concerns/service_params.rb b/app/controllers/concerns/service_params.rb
index e610ccaec96..2992568ae66 100644
--- a/app/controllers/concerns/service_params.rb
+++ b/app/controllers/concerns/service_params.rb
@@ -33,6 +33,7 @@ module ServiceParams
:issues_url,
:jira_issue_transition_id,
:merge_requests_events,
+ :mock_service_url,
:namespace,
:new_issue_url,
:notify,
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index ca5e81100da..1151555b8fa 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -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
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index d122c7fdcb2..53f30a24312 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -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])
- 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])
+ respond_to do |format|
+ format.html do
+ if @merge_request.valid?
+ redirect_to([@merge_request.target_project.namespace.becomes(Namespace), @merge_request.target_project, @merge_request])
+ else
+ render :edit
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
rescue ActiveRecord::StaleObjectError
- @conflict = true
- render :edit
+ render_conflict_response
end
def remove_wip
diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb
index 4c7c16d694c..195094730aa 100644
--- a/app/helpers/button_helper.rb
+++ b/app/helpers/button_helper.rb
@@ -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,
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 735a355c25a..4befeacc135 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -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
diff --git a/app/models/event.rb b/app/models/event.rb
index 4b8eac9accf..d7ca8e3c599 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -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) }
diff --git a/app/models/project.rb b/app/models/project.rb
index aedd5bedcb9..e06fc30dc8a 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -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
diff --git a/app/models/project_services/mattermost_service.rb b/app/models/project_services/mattermost_service.rb
index 4ebc5318da1..c13538e9fea 100644
--- a/app/models/project_services/mattermost_service.rb
+++ b/app/models/project_services/mattermost_service.rb
@@ -15,10 +15,10 @@ class MattermostService < ChatNotificationService
'This service sends notifications about projects events to Mattermost channels.<br />
To set up this service:
<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><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 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
diff --git a/app/models/project_services/mock_ci_service.rb b/app/models/project_services/mock_ci_service.rb
new file mode 100644
index 00000000000..a8d581a1f67
--- /dev/null
+++ b/app/models/project_services/mock_ci_service.rb
@@ -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
diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb
index f77d2d7c60b..da7496573ef 100644
--- a/app/models/project_services/slack_service.rb
+++ b/app/models/project_services/slack_service.rb
@@ -13,11 +13,11 @@ class SlackService < ChatNotificationService
def help
'This service sends notifications about projects events to Slack channels.<br />
- To setup this service:
+ To set up this service:
<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><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 <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
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 1762118278a..0dbf246c3a4 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -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
}
diff --git a/app/models/service.rb b/app/models/service.rb
index facaaf9b331..3ef4cbead10 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -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)
diff --git a/app/services/groups/destroy_service.rb b/app/services/groups/destroy_service.rb
index 2e2d7f884ac..497fdb09cdc 100644
--- a/app/services/groups/destroy_service.rb
+++ b/app/services/groups/destroy_service.rb
@@ -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!
diff --git a/app/views/admin/runners/_runner.html.haml b/app/views/admin/runners/_runner.html.haml
index deb62845e1c..d4d166ab7b6 100644
--- a/app/views/admin/runners/_runner.html.haml
+++ b/app/views/admin/runners/_runner.html.haml
@@ -15,6 +15,8 @@
%td
= runner.description
%td
+ = runner.version
+ %td
- if runner.shared?
n/a
- else
diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml
index d725e477044..7d26864d0f3 100644
--- a/app/views/admin/runners/index.html.haml
+++ b/app/views/admin/runners/index.html.haml
@@ -67,6 +67,7 @@
%th Type
%th Runner token
%th Description
+ %th Version
%th Projects
%th Jobs
%th Tags
diff --git a/app/views/dashboard/_activities.html.haml b/app/views/dashboard/_activities.html.haml
index dc76599b776..0dbb0ca6958 100644
--- a/app/views/dashboard/_activities.html.haml
+++ b/app/views/dashboard/_activities.html.haml
@@ -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'
diff --git a/app/views/groups/_activities.html.haml b/app/views/groups/_activities.html.haml
index 71cc4d87b1f..c442cf056c3 100644
--- a/app/views/groups/_activities.html.haml
+++ b/app/views/groups/_activities.html.haml
@@ -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'
diff --git a/app/views/projects/_activity.html.haml b/app/views/projects/_activity.html.haml
index 0ea733cb978..4268337fd6d 100644
--- a/app/views/projects/_activity.html.haml
+++ b/app/views/projects/_activity.html.haml
@@ -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'
diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml
deleted file mode 100644
index 3475fa5f960..00000000000
--- a/app/views/projects/ci/pipelines/_pipeline.html.haml
+++ /dev/null
@@ -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")
diff --git a/changelogs/unreleased/14748-runner-version-in-admin-views.yml b/changelogs/unreleased/14748-runner-version-in-admin-views.yml
new file mode 100644
index 00000000000..2478a81c824
--- /dev/null
+++ b/changelogs/unreleased/14748-runner-version-in-admin-views.yml
@@ -0,0 +1,4 @@
+---
+title: Add runner version to /admin/runners view
+merge_request: 8733
+author: Jonathon Reinhart
diff --git a/changelogs/unreleased/1937-https-clone-url-username.yml b/changelogs/unreleased/1937-https-clone-url-username.yml
new file mode 100644
index 00000000000..fa89d94e0f3
--- /dev/null
+++ b/changelogs/unreleased/1937-https-clone-url-username.yml
@@ -0,0 +1,4 @@
+---
+title: Add the Username to the HTTP(S) clone URL of a Repository
+merge_request: 9347
+author: Jan Christophersen
diff --git a/changelogs/unreleased/23062-allow-git-log-to-accept-follow-and-skip.yml b/changelogs/unreleased/23062-allow-git-log-to-accept-follow-and-skip.yml
new file mode 100644
index 00000000000..f7c856040e0
--- /dev/null
+++ b/changelogs/unreleased/23062-allow-git-log-to-accept-follow-and-skip.yml
@@ -0,0 +1,4 @@
+---
+title: Make Git history follow renames again by performing the --skip in Ruby
+merge_request:
+author:
diff --git a/changelogs/unreleased/27267-events-project-query-performance-regression.yml b/changelogs/unreleased/27267-events-project-query-performance-regression.yml
new file mode 100644
index 00000000000..a1697b57eac
--- /dev/null
+++ b/changelogs/unreleased/27267-events-project-query-performance-regression.yml
@@ -0,0 +1,4 @@
+---
+title: 'Add performance query regression fix for !9088 affecting #27267'
+merge_request:
+author:
diff --git a/changelogs/unreleased/feature-runners-registration-deletion-v4-api.yml b/changelogs/unreleased/feature-runners-registration-deletion-v4-api.yml
new file mode 100644
index 00000000000..e646a6a17b7
--- /dev/null
+++ b/changelogs/unreleased/feature-runners-registration-deletion-v4-api.yml
@@ -0,0 +1,4 @@
+---
+title: Add Runner's registration/deletion v4 API
+merge_request: 9246
+author:
diff --git a/changelogs/unreleased/issue_24815.yml b/changelogs/unreleased/issue_24815.yml
new file mode 100644
index 00000000000..916e47d36a9
--- /dev/null
+++ b/changelogs/unreleased/issue_24815.yml
@@ -0,0 +1,4 @@
+---
+title: Fix issuable stale object error handler for js when updating tasklists
+merge_request:
+author:
diff --git a/changelogs/unreleased/mock-ci-service.yml b/changelogs/unreleased/mock-ci-service.yml
new file mode 100644
index 00000000000..24c6366177f
--- /dev/null
+++ b/changelogs/unreleased/mock-ci-service.yml
@@ -0,0 +1,4 @@
+---
+title: Add Mock CI service/integration for development
+merge_request:
+author:
diff --git a/changelogs/unreleased/mr-diff-comment-button.yml b/changelogs/unreleased/mr-diff-comment-button.yml
new file mode 100644
index 00000000000..1dc6ed1c495
--- /dev/null
+++ b/changelogs/unreleased/mr-diff-comment-button.yml
@@ -0,0 +1,4 @@
+---
+title: Improved diff comment button UX
+merge_request:
+author:
diff --git a/changelogs/unreleased/rss-btn-alignment-fix.yml b/changelogs/unreleased/rss-btn-alignment-fix.yml
new file mode 100644
index 00000000000..c8f57ec0b7c
--- /dev/null
+++ b/changelogs/unreleased/rss-btn-alignment-fix.yml
@@ -0,0 +1,4 @@
+---
+title: Fixed RSS button alignment on activity pages
+merge_request:
+author:
diff --git a/changelogs/unreleased/sh-bump-hashie-to-3-5-5.yml b/changelogs/unreleased/sh-bump-hashie-to-3-5-5.yml
new file mode 100644
index 00000000000..57f1474093a
--- /dev/null
+++ b/changelogs/unreleased/sh-bump-hashie-to-3-5-5.yml
@@ -0,0 +1,4 @@
+---
+title: Bump Hashie to 3.5.5 and omniauth to 1.4.2 to eliminate warning noise
+merge_request:
+author:
diff --git a/changelogs/unreleased/ssh-key-paste.yml b/changelogs/unreleased/ssh-key-paste.yml
new file mode 100644
index 00000000000..1e34ef60f6e
--- /dev/null
+++ b/changelogs/unreleased/ssh-key-paste.yml
@@ -0,0 +1,4 @@
+---
+title: SSH key field updates title after pasting key
+merge_request:
+author:
diff --git a/changelogs/unreleased/unified-member-api-response.yml b/changelogs/unreleased/unified-member-api-response.yml
new file mode 100644
index 00000000000..0a60b4d46a3
--- /dev/null
+++ b/changelogs/unreleased/unified-member-api-response.yml
@@ -0,0 +1,4 @@
+---
+title: 'API: Return 400 for all validation erros in the mebers API'
+merge_request: 9523
+author: Robert Schilling
diff --git a/changelogs/unreleased/update-vue-2-1.yml b/changelogs/unreleased/update-vue-2-1.yml
new file mode 100644
index 00000000000..acc42bf00b1
--- /dev/null
+++ b/changelogs/unreleased/update-vue-2-1.yml
@@ -0,0 +1,4 @@
+---
+title: update Vue to v2.1.10
+merge_request: 9386
+author:
diff --git a/config/webpack.config.js b/config/webpack.config.js
index ddc888c5f93..d178106d179 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -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',
}
}
}
diff --git a/doc/administration/container_registry.md b/doc/administration/container_registry.md
index a6300e18dc0..28e413ef447 100644
--- a/doc/administration/container_registry.md
+++ b/doc/administration/container_registry.md
@@ -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
diff --git a/doc/api/services.md b/doc/api/services.md
index fba5da6587d..b030a425a7a 100644
--- a/doc/api/services.md
+++ b/doc/api/services.md
@@ -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
+```
diff --git a/doc/api/v3_to_v4.md b/doc/api/v3_to_v4.md
index 8af041be234..c178e224cc5 100644
--- a/doc/api/v3_to_v4.md
+++ b/doc/api/v3_to_v4.md
@@ -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) \ No newline at end of file
+- 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)
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index dd3ba1283f8..a586b095ef5 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -1018,7 +1018,7 @@ A simple example:
```yaml
job1:
- coverage: /Code coverage: \d+\.\d+/
+ coverage: '/Code coverage: \d+\.\d+/'
```
## Git Strategy
diff --git a/doc/development/ci_setup.md b/doc/development/ci_setup.md
index 2f49b3564ab..b03216fec95 100644
--- a/doc/development/ci_setup.md
+++ b/doc/development/ci_setup.md
@@ -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 |
|---------------------------------------|---------------------------|---------------------------------------------------------------------------|-----------|
diff --git a/doc/user/project/integrations/img/mattermost_configuration.png b/doc/user/project/integrations/img/mattermost_configuration.png
index 3c5ff5ee317..f52acf4ef3b 100644
--- a/doc/user/project/integrations/img/mattermost_configuration.png
+++ b/doc/user/project/integrations/img/mattermost_configuration.png
Binary files differ
diff --git a/doc/user/project/integrations/img/slack_configuration.png b/doc/user/project/integrations/img/slack_configuration.png
index fc8e58e686b..527824fc3eb 100644
--- a/doc/user/project/integrations/img/slack_configuration.png
+++ b/doc/user/project/integrations/img/slack_configuration.png
Binary files differ
diff --git a/doc/user/project/integrations/mattermost.md b/doc/user/project/integrations/mattermost.md
index 09ba9994d3a..cfb0931273d 100644
--- a/doc/user/project/integrations/mattermost.md
+++ b/doc/user/project/integrations/mattermost.md
@@ -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)
diff --git a/doc/user/project/integrations/mock_ci.md b/doc/user/project/integrations/mock_ci.md
new file mode 100644
index 00000000000..6aefe5dbded
--- /dev/null
+++ b/doc/user/project/integrations/mock_ci.md
@@ -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)
diff --git a/doc/user/project/integrations/slack.md b/doc/user/project/integrations/slack.md
index 57a9492044b..f27f9a726fc 100644
--- a/doc/user/project/integrations/slack.md
+++ b/doc/user/project/integrations/slack.md
@@ -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.
diff --git a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
index 8c5020bee37..9cc45065eb2 100644
--- a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
+++ b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
@@ -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
diff --git a/features/steps/explore/projects.rb b/features/steps/explore/projects.rb
index 2b4a5ab0864..7dc33ab5683 100644
--- a/features/steps/explore/projects.rb
+++ b/features/steps/explore/projects.rb
@@ -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
diff --git a/lib/api/api.rb b/lib/api/api.rb
index dc732012a33..7aa95a4a3c1 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -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
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 0e37f40a887..a99d9cadc8a 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -618,6 +618,10 @@ module API
end
end
+ class RunnerRegistrationDetails < Grape::Entity
+ expose :id, :token
+ end
+
class BuildArtifactFile < Grape::Entity
expose :filename, :size
end
diff --git a/lib/api/helpers/runner.rb b/lib/api/helpers/runner.rb
new file mode 100644
index 00000000000..119ca81b883
--- /dev/null
+++ b/lib/api/helpers/runner.rb
@@ -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
diff --git a/lib/api/members.rb b/lib/api/members.rb
index 8360c007005..5f6913d1a27 100644
--- a/lib/api/members.rb
+++ b/lib/api/members.rb
@@ -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
diff --git a/lib/api/runner.rb b/lib/api/runner.rb
new file mode 100644
index 00000000000..804b27d40a7
--- /dev/null
+++ b/lib/api/runner.rb
@@ -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
diff --git a/lib/api/services.rb b/lib/api/services.rb
index 1456fe4688b..ad856115485 100644
--- a/lib/api/services.rb
+++ b/lib/api/services.rb
@@ -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' => [
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index b8354d7bf04..8ec90885231 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -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?
-
- raw_output = IO.popen(cmd) {|io| io.read }
-
- log = raw_output.lines.map do |c|
- Rugged::Commit.new(rugged, c.strip)
- end
-
- log.is_a?(Array) ? log : []
+ 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
+
+ lines.map! { |c| Rugged::Commit.new(rugged, c.strip) }
end
def sha_from_ref(ref)
diff --git a/package.json b/package.json
index ddbf4a3293c..48cff0d7661 100644
--- a/package.json
+++ b/package.json
@@ -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"
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index e493b9396f6..46c758b4654 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -125,14 +125,16 @@ describe Projects::IssuesController do
end
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
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
before { another_project.team << [user, :reporter] }
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index d9cb429132f..1ced666bb36 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -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
diff --git a/spec/features/admin/admin_disables_git_access_protocol_spec.rb b/spec/features/admin/admin_disables_git_access_protocol_spec.rb
index e8e080ce3e2..273cacd82cd 100644
--- a/spec/features/admin/admin_disables_git_access_protocol_spec.rb
+++ b/spec/features/admin/admin_disables_git_access_protocol_spec.rb
@@ -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
diff --git a/spec/features/profiles/keys_spec.rb b/spec/features/profiles/keys_spec.rb
index eb1050d21c6..2f436f153aa 100644
--- a/spec/features/profiles/keys_spec.rb
+++ b/spec/features/profiles/keys_spec.rb
@@ -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
diff --git a/spec/features/projects/developer_views_empty_project_instructions_spec.rb b/spec/features/projects/developer_views_empty_project_instructions_spec.rb
index 0c51fe72ca4..2352329d58c 100644
--- a/spec/features/projects/developer_views_empty_project_instructions_spec.rb
+++ b/spec/features/projects/developer_views_empty_project_instructions_spec.rb
@@ -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"
-
- expect(page).to have_content("git clone #{project.send(msg)}")
+ 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 #{url}")
end
end
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index 766f16bb824..1919542ca25 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -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
+ 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
- options = { ref: "master", follow: true }
+ let(:options) { { ref: "master", follow: true } }
context "and 'path' is a directory" do
- let(:log_commits) do
- repository.log(options.merge(path: "encoding"))
- end
+ it "does not follow renames" do
+ log_commits = repository.log(options.merge(path: "encoding"))
- it "should not follow renames" 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)
+ 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
end
context "and 'path' is a file that matches the new filename" do
- let(:log_commits) do
- repository.log(options.merge(path: "encoding/CHANGELOG"))
+ 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
end
- it "should follow renames" 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)
+ 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
- let(:log_commits) do
- repository.log(options.merge(path: "CHANGELOG"))
- end
+ it "does not follow renames" do
+ log_commits = repository.log(options.merge(path: "CHANGELOG"))
- it "should not follow renames" do
- expect(log_commits).to include(commit_with_old_name)
- expect(log_commits).to include(rename_commit)
- expect(log_commits).not_to include(commit_with_new_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 "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
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 5232c531635..ee4f4092062 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -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
diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb
index 31166b50033..127498ed109 100644
--- a/spec/requests/api/members_spec.rb
+++ b/spec/requests/api/members_spec.rb
@@ -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
diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb
new file mode 100644
index 00000000000..73e82647ca0
--- /dev/null
+++ b/spec/requests/api/runner_spec.rb
@@ -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
diff --git a/spec/services/groups/destroy_service_spec.rb b/spec/services/groups/destroy_service_spec.rb
index 32c2ed8cae7..98c560ffb26 100644
--- a/spec/services/groups/destroy_service_spec.rb
+++ b/spec/services/groups/destroy_service_spec.rb
@@ -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
diff --git a/spec/support/update_invalid_issuable.rb b/spec/support/update_invalid_issuable.rb
new file mode 100644
index 00000000000..f984ac7bfa7
--- /dev/null
+++ b/spec/support/update_invalid_issuable.rb
@@ -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
diff --git a/yarn.lock b/yarn.lock
index 565fe687a36..e4e5c40089b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -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"