diff options
author | Filipa Lacerda <lacerda.filipa@gmail.com> | 2017-02-27 17:10:25 +0000 |
---|---|---|
committer | Filipa Lacerda <lacerda.filipa@gmail.com> | 2017-02-27 17:10:25 +0000 |
commit | 61f65992a05e8fc5ab756ac0ce890d090c6cf4eb (patch) | |
tree | 57952a5fa69f757bcd33247e6e3642ba98c6252b | |
parent | 44622abe9603c5419bc50212654f737343012ca8 (diff) | |
parent | 883342ce36b5f38156a3376af7d8a2e274163917 (diff) | |
download | gitlab-ce-61f65992a05e8fc5ab756ac0ce890d090c6cf4eb.tar.gz |
Merge branch 'master' into 'add-svg-loader'
# Conflicts:
# app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6
85 files changed, 1105 insertions, 479 deletions
@@ -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 Binary files differindex 3c5ff5ee317..f52acf4ef3b 100644 --- a/doc/user/project/integrations/img/mattermost_configuration.png +++ b/doc/user/project/integrations/img/mattermost_configuration.png diff --git a/doc/user/project/integrations/img/slack_configuration.png b/doc/user/project/integrations/img/slack_configuration.png Binary files differindex fc8e58e686b..527824fc3eb 100644 --- a/doc/user/project/integrations/img/slack_configuration.png +++ b/doc/user/project/integrations/img/slack_configuration.png 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" |