diff options
111 files changed, 1331 insertions, 326 deletions
@@ -32,6 +32,7 @@ gem 'omniauth-saml', '~> 1.7.0' gem 'omniauth-shibboleth', '~> 1.2.0' gem 'omniauth-twitter', '~> 1.2.0' gem 'omniauth_crowd', '~> 2.2.0' +gem 'omniauth-authentiq', '~> 0.2.0' gem 'rack-oauth2', '~> 1.2.1' gem 'jwt' diff --git a/Gemfile.lock b/Gemfile.lock index 8bc22479346..9f8367b420a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -428,6 +428,8 @@ GEM rack (>= 1.0, < 3) omniauth-auth0 (1.4.1) omniauth-oauth2 (~> 1.1) + omniauth-authentiq (0.2.2) + omniauth-oauth2 (~> 1.3, >= 1.3.1) omniauth-azure-oauth2 (0.0.6) jwt (~> 1.0) omniauth (~> 1.0) @@ -897,6 +899,7 @@ DEPENDENCIES oj (~> 2.17.4) omniauth (~> 1.3.1) omniauth-auth0 (~> 1.4.1) + omniauth-authentiq (~> 0.2.0) omniauth-azure-oauth2 (~> 0.0.6) omniauth-cas3 (~> 1.1.2) omniauth-facebook (~> 4.0.0) diff --git a/app/assets/images/auth_buttons/authentiq_64.png b/app/assets/images/auth_buttons/authentiq_64.png Binary files differnew file mode 100644 index 00000000000..81767bbcc54 --- /dev/null +++ b/app/assets/images/auth_buttons/authentiq_64.png diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6 index 1e259a16f06..752f35e6356 100644 --- a/app/assets/javascripts/dispatcher.js.es6 +++ b/app/assets/javascripts/dispatcher.js.es6 @@ -141,6 +141,11 @@ case 'projects:merge_requests:builds': new MergedButtons(); break; + case 'projects:merge_requests:pipelines': + new gl.MiniPipelineGraph({ + container: '.js-pipeline-table', + }); + break; case "projects:merge_requests:diffs": new gl.Diff(); new ZenMode(); @@ -158,6 +163,11 @@ new ZenMode(); shortcut_handler = new ShortcutsNavigation(); break; + case 'projects:commit:pipelines': + new gl.MiniPipelineGraph({ + container: '.js-pipeline-table', + }); + break; case 'projects:commit:builds': new gl.Pipelines(); break; @@ -172,6 +182,11 @@ new TreeView(); } break; + case 'projects:pipelines:index': + new gl.MiniPipelineGraph({ + container: '.js-pipeline-table', + }); + break; case 'projects:pipelines:builds': case 'projects:pipelines:show': const { controllerAction } = document.querySelector('.js-pipeline-container').dataset; diff --git a/app/assets/javascripts/gfm_auto_complete.js.es6 b/app/assets/javascripts/gfm_auto_complete.js.es6 index 17d03c87bf5..64e6258c154 100644 --- a/app/assets/javascripts/gfm_auto_complete.js.es6 +++ b/app/assets/javascripts/gfm_auto_complete.js.es6 @@ -379,14 +379,7 @@ togglePreventSelection(isPrevented = !!this.setting.tabSelectsMatch) { this.setting.tabSelectsMatch = !isPrevented; this.setting.spaceSelectsMatch = !isPrevented; - const eventListenerAction = `${isPrevented ? 'add' : 'remove'}EventListener`; - this.$inputor[0][eventListenerAction]('keydown', gl.GfmAutoComplete.preventSpaceTabEnter); }, - preventSpaceTabEnter(e) { - const key = e.which || e.keyCode; - const preventables = [9, 13, 32]; - if (preventables.indexOf(key) > -1) e.preventDefault(); - } }; }).call(this); diff --git a/app/assets/javascripts/issuable.js.es6 b/app/assets/javascripts/issuable.js.es6 index 1c10a7445bb..9c3c96c20ed 100644 --- a/app/assets/javascripts/issuable.js.es6 +++ b/app/assets/javascripts/issuable.js.es6 @@ -1,13 +1,13 @@ -/* eslint-disable func-names, no-var, camelcase, no-unused-vars, object-shorthand, space-before-function-paren, no-return-assign, comma-dangle, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, prefer-arrow-callback, prefer-const, padded-blocks, wrap-iife, max-len */ +/* eslint-disable no-param-reassign, func-names, no-var, camelcase, no-unused-vars, object-shorthand, space-before-function-paren, no-return-assign, comma-dangle, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, prefer-arrow-callback, prefer-const, padded-blocks, wrap-iife, max-len */ /* global Issuable */ /* global Turbolinks */ -(function() { +((global) => { var issuable_created; issuable_created = false; - this.Issuable = { + global.Issuable = { init: function() { Issuable.initTemplates(); Issuable.initSearch(); @@ -111,7 +111,11 @@ filterResults: (function(_this) { return function(form) { var formAction, formData, issuesUrl; - formData = form.serialize(); + formData = form.serializeArray(); + formData = formData.filter(function(data) { + return data.value !== ''; + }); + formData = $.param(formData); formAction = form.attr('action'); issuesUrl = formAction; issuesUrl += "" + (formAction.indexOf('?') < 0 ? '?' : '&'); @@ -184,4 +188,4 @@ } }; -}).call(this); +})(window); diff --git a/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6 b/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6 new file mode 100644 index 00000000000..90b3366f14b --- /dev/null +++ b/app/assets/javascripts/mini_pipeline_graph_dropdown.js.es6 @@ -0,0 +1,96 @@ +/* eslint-disable no-new */ +/* global Flash */ + +/** + * In each pipelines table we have a mini pipeline graph for each pipeline. + * + * When we click in a pipeline stage, we need to make an API call to get the + * builds list to render in a dropdown. + * + * The container should be the table element. + * + * The stage icon clicked needs to have the following HTML structure: + * <div> + * <button class="dropdown js-builds-dropdown-button"></button> + * <div class="js-builds-dropdown-container"></div> + * </div> + */ +(() => { + class MiniPipelineGraph { + constructor(opts = {}) { + this.container = opts.container || ''; + this.dropdownListSelector = '.js-builds-dropdown-container'; + this.getBuildsList = this.getBuildsList.bind(this); + + this.bindEvents(); + } + + /** + * Adds and removes the event listener. + */ + bindEvents() { + const dropdownButtonSelector = 'button.js-builds-dropdown-button'; + + $(this.container).off('click', dropdownButtonSelector, this.getBuildsList) + .on('click', dropdownButtonSelector, this.getBuildsList); + } + + /** + * For the clicked stage, renders the given data in the dropdown list. + * + * @param {HTMLElement} stageContainer + * @param {Object} data + */ + renderBuildsList(stageContainer, data) { + const dropdownContainer = stageContainer.parentElement.querySelector( + `${this.dropdownListSelector} .js-builds-dropdown-list`, + ); + + dropdownContainer.innerHTML = data; + } + + /** + * For the clicked stage, gets the list of builds. + * + * @param {Object} e + * @return {Promise} + */ + getBuildsList(e) { + const button = e.currentTarget; + const endpoint = button.dataset.stageEndpoint; + + return $.ajax({ + dataType: 'json', + type: 'GET', + url: endpoint, + beforeSend: () => { + this.renderBuildsList(button, ''); + this.toggleLoading(button); + }, + success: (data) => { + this.toggleLoading(button); + this.renderBuildsList(button, data.html); + }, + error: () => { + this.toggleLoading(button); + new Flash('An error occurred while fetching the builds.', 'alert'); + }, + }); + } + + /** + * Toggles the visibility of the loading icon. + * + * @param {HTMLElement} stageContainer + * @return {type} + */ + toggleLoading(stageContainer) { + stageContainer.parentElement.querySelector( + `${this.dropdownListSelector} .js-builds-dropdown-loading`, + ).classList.toggle('hidden'); + } + } + + window.gl = window.gl || {}; + window.gl.MiniPipelineGraph = MiniPipelineGraph; +})(); diff --git a/app/assets/javascripts/network/branch_graph.js b/app/assets/javascripts/network/branch_graph.js index 64b19a54893..20a68780cd5 100644 --- a/app/assets/javascripts/network/branch_graph.js +++ b/app/assets/javascripts/network/branch_graph.js @@ -356,7 +356,7 @@ icon = this.image(gon.relative_url_root + commit.author.icon, x, y, 20, 20); nameText = this.text(x + 25, y + 10, commit.author.name); idText = this.text(x, y + 35, commit.id); - messageText = this.text(x, y + 50, commit.message); + messageText = this.text(x, y + 50, commit.message.replace(/\r?\n/g, " \n ")); textSet = this.set(icon, nameText, idText, messageText).attr({ "text-anchor": "start", font: "12px Monaco, monospace" @@ -368,6 +368,7 @@ idText.attr({ fill: "#AAA" }); + messageText.node.style["white-space"] = "pre"; this.textWrap(messageText, boxWidth - 50); rect = this.rect(x - 10, y - 10, boxWidth, 100, 4).attr({ fill: "#FFF", @@ -404,16 +405,21 @@ s.push("\n"); x = 0; } - x += word.length * letterWidth; - s.push(word + " "); + if (word === "\n") { + s.push("\n"); + x = 0; + } else { + s.push(word + " "); + x += word.length * letterWidth; + } } t.attr({ - text: s.join("") + text: s.join("").trim() }); b = t.getBBox(); - h = Math.abs(b.y2) - Math.abs(b.y) + 1; + h = Math.abs(b.y2) + 1; return t.attr({ - y: b.y + h + y: h }); }; diff --git a/app/assets/javascripts/u2f/authenticate.js b/app/assets/javascripts/u2f/authenticate.js index d2aa3c7a841..e407b856e10 100644 --- a/app/assets/javascripts/u2f/authenticate.js +++ b/app/assets/javascripts/u2f/authenticate.js @@ -89,7 +89,8 @@ U2FAuthenticate.prototype.renderError = function(error) { this.renderTemplate('error', { - error_message: error.message() + error_message: error.message(), + error_code: error.errorCode }); return this.container.find('#js-u2f-try-again').on('click', this.renderSetup); }; diff --git a/app/assets/javascripts/u2f/error.js b/app/assets/javascripts/u2f/error.js index 69f98c9c0ad..bb9942a3aa0 100644 --- a/app/assets/javascripts/u2f/error.js +++ b/app/assets/javascripts/u2f/error.js @@ -9,7 +9,6 @@ this.errorCode = errorCode; this.message = bind(this.message, this); this.httpsDisabled = window.location.protocol !== 'https:'; - console.error("U2F Error Code: " + this.errorCode); } U2FError.prototype.message = function() { diff --git a/app/assets/javascripts/u2f/register.js b/app/assets/javascripts/u2f/register.js index 4f5d68f546b..050c9bfc02e 100644 --- a/app/assets/javascripts/u2f/register.js +++ b/app/assets/javascripts/u2f/register.js @@ -76,7 +76,8 @@ U2FRegister.prototype.renderError = function(error) { this.renderTemplate('error', { - error_message: error.message() + error_message: error.message(), + error_code: error.errorCode }); return this.container.find('#js-u2f-try-again').on('click', this.renderSetup); }; diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index 40bc0579393..3cf49f4ff1b 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -9,6 +9,7 @@ @import "framework/asciidoctor.scss"; @import "framework/blocks.scss"; @import "framework/buttons.scss"; +@import "framework/badges.scss"; @import "framework/calendar.scss"; @import "framework/callout.scss"; @import "framework/common.scss"; diff --git a/app/assets/stylesheets/framework/badges.scss b/app/assets/stylesheets/framework/badges.scss new file mode 100644 index 00000000000..e9d7cda0647 --- /dev/null +++ b/app/assets/stylesheets/framework/badges.scss @@ -0,0 +1,11 @@ +.badge { + font-weight: normal; + background-color: $badge-bg; + color: $badge-color; + vertical-align: baseline; +} + +.badge-dark { + background-color: $badge-bg-dark; + color: $badge-color-dark; +} diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss index 59fae61a44f..5365b62e456 100644 --- a/app/assets/stylesheets/framework/layout.scss +++ b/app/assets/stylesheets/framework/layout.scss @@ -33,10 +33,12 @@ body { } .alert-wrapper { - margin-bottom: $gl-padding; - .alert { margin-bottom: 0; + + &:last-child { + margin-bottom: $gl-padding; + } } /* Stripe the background colors so that adjacent alert-warnings are distinct from one another */ diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index e4affbb1be1..bbf9de06630 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -76,13 +76,6 @@ color: $black; } } - - .badge { - font-weight: normal; - background-color: $nav-badge-bg; - color: $gl-gray-light; - vertical-align: baseline; - } } &.sub-nav { @@ -434,4 +427,4 @@ border-bottom: none; } } -} +}
\ No newline at end of file diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 460c5d995be..a5fb097a47f 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -280,6 +280,14 @@ $btn-active-gray: #ececec; $btn-active-gray-light: e4e7ed; /* +* Badges +*/ +$badge-bg: #f3f3f3; +$badge-bg-dark: #eee; +$badge-color: #929292; +$badge-color-dark: #8f8f8f; + +/* * Award emoji */ $award-emoji-menu-shadow: rgba(0,0,0,.175); diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index 3d60426de01..5517dc5dcbd 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -121,13 +121,6 @@ .folder-name { cursor: pointer; - - .badge { - font-weight: normal; - background-color: $gray-darker; - color: $gl-gray-light; - vertical-align: baseline; - } } } @@ -142,4 +135,4 @@ margin-right: 0; } } -} +}
\ No newline at end of file diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index c9d54b4f3d3..566de8a4eba 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -22,17 +22,22 @@ .table.ci-table { min-width: 1200px; + table-layout: fixed; .pipeline-id { color: $black; } - .branch-commit { - width: 30%; + .pipeline-date, + .pipeline-status { + width: 10%; + } - .branch-name { - max-width: 195px; - } + .pipeline-info, + .pipeline-commit, + .pipeline-actions, + .pipeline-stages { + width: 20%; } } } @@ -106,7 +111,7 @@ .branch-name { font-weight: bold; - max-width: 150px; + max-width: 120px; overflow: hidden; display: inline-block; white-space: nowrap; @@ -132,7 +137,7 @@ .commit-title { margin-top: 4px; - max-width: 300px; + max-width: 225px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; @@ -192,10 +197,6 @@ border-bottom: 2px solid $border-color; } } - - a { - display: block; - } } } @@ -462,6 +463,25 @@ white-space: normal; color: $gl-text-color-light; + .dropdown-menu-toggle { + background-color: transparent; + border: none; + padding: 0; + color: $gl-text-color-light; + + &:focus { + outline: none; + } + + &:hover { + color: $gl-text-color; + + .dropdown-counter-badge { + color: $gl-text-color; + } + } + } + > .build-content { display: inline-block; padding: 8px 10px 9px; @@ -527,7 +547,7 @@ content: ''; position: absolute; top: 48%; - right: -49px; + right: -48px; border-top: 2px solid $border-color; width: 48px; height: 1px; @@ -574,156 +594,280 @@ } } } +} - .ci-status-text { - max-width: 110px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - vertical-align: bottom; +.dropdown-counter-badge { + float: right; + color: $border-color; + font-weight: 100; + font-size: 15px; + margin-right: 2px; +} + +.grouped-pipeline-dropdown { + padding: 0; + width: 191px; + left: auto; + right: -195px; + top: -4px; + box-shadow: 0 1px 5px $black-transparent; + + a { display: inline-block; - position: relative; - font-weight: 100; + + &:hover { + background-color: $stage-hover-bg; + } } - .dropdown-menu-toggle { - background-color: transparent; - border: none; - padding: 0; - color: $gl-text-color-light; - white-space: normal; - overflow: visible; + ul { + max-height: 245px; + overflow: auto; + margin: 5px 0; - &:focus { - outline: none; + li { + padding-top: 2px; + margin: 0 5px; + padding-left: 0; + padding-bottom: 0; + margin-bottom: 0; + line-height: 1.2; + } + } +} + +.ci-status-text { + max-width: 110px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + vertical-align: bottom; + display: inline-block; + position: relative; + font-weight: 100; +} + +// Action Icons +.ci-action-icon-container .ci-action-icon-wrapper { + i { + color: $border-color; + border-radius: 100%; + border: 1px solid $border-color; + padding: 5px 6px; + font-size: 13px; + background: $white-light; + height: 30px; + width: 30px; + + &::before { + position: relative; + top: 3px; + left: 3px; } &:hover { color: $gl-text-color; + background-color: $stage-hover-bg; + border: 1px solid $stage-hover-bg; + } + } - .dropdown-counter-badge { - color: $gl-text-color; + .ci-play-icon { + padding: 5px 5px 5px 7px; + } +} + +.dropdown-build { + color: $gl-text-color-light; + + .ci-action-icon-container { + padding: 0; + font-size: 11px; + float: right; + margin-top: 4px; + display: inline-block; + position: relative; + + i { + font-size: 11px; + margin-top: 0; + } + } + + &:hover { + background-color: $stage-hover-bg; + border-radius: 3px; + color: $gl-text-color; + } + + .ci-action-icon-container { + i { + width: 25px; + height: 25px; + + &::before { + top: 1px; + left: 1px; } } } - .dropdown-counter-badge { - float: right; - clear: right; - color: $border-color; - font-weight: 100; - font-size: 15px; - margin-right: 2px; + .stage { + max-width: 100px; + width: 100px; } - .grouped-pipeline-dropdown { + .ci-status-icon svg { + height: 18px; + width: 18px; + } + + .ci-status-text { + max-width: 95px; + } +} + +/** + * Builds dropdown in mini pipeline + */ +.mini-pipeline-graph { + .builds-dropdown { + background-color: transparent; + border: none; padding: 0; - width: 191px; - left: auto; - right: -195px; - top: -4px; - box-shadow: 0 1px 5px $black-transparent; + color: $gl-text-color-light; + border: none; + margin: 0; + } + + .builds-dropdown-loading { + margin: 10px auto; + width: 18px; + } + + .grouped-pipeline-dropdown { + right: -172px; + top: 23px; + min-height: 50px; a { - display: inline-block; + color: $gl-text-color-light; + } + } - &:hover { - background-color: $stage-hover-bg; - } + .arrow-up { + &::before, + &::after { + content: ''; + display: inline-block; + position: absolute; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; + top: -6px; + left: 2px; + border-width: 0 5px 6px; } - ul { - max-height: 245px; - overflow: auto; - margin: 5px 0; - - li { - margin: 0 5px; - padding-left: 0; - padding-bottom: 0; - margin-bottom: 0; - line-height: 1.2; - } + &::before { + border-width: 0 5px 5px; + border-bottom-color: $border-color; } - .dropdown-build { - color: $gl-text-color-light; + &::after { + margin-top: 1px; + border-bottom-color: $white-light; + } + } +} - .build-content { - width: 100%; - } +/** + * Icons in mini pipeline graph + */ +.mini-pipeline-graph-icon-container .ci-status-icon { + display: inline-block; + border: 1px solid; + border-radius: 20px; + margin-right: 1px; + width: 20px; + height: 20px; + position: relative; + z-index: 2; + transition: all 0.2s cubic-bezier(0.25, 0, 1, 1); - .ci-action-icon-container { - font-size: 11px; - position: absolute; - right: 4px; + svg { + top: -1px; + } +} - i { - width: 25px; - height: 25px; - font-size: 11px; - margin-top: 0; +.builds-dropdown { + &:focus { + outline: none; + margin-right: -8px; - &::before { - top: 1px; - left: 1px; - } - } - } + .ci-status-icon { + width: 28px; + padding: 0 8px 0 0; + transition: width 0.2s cubic-bezier(0.25, 0, 1, 1); - &:hover { - background-color: $stage-hover-bg; - border-radius: 3px; - color: $gl-text-color; + + .dropdown-caret { + display: inline-block; } + } + } - .stage { - max-width: 100px; - width: 100px; - } + &:focus, + &:active { + .ci-status-icon-success { + background-color: rgba($gl-success, .1); + } - .ci-status-icon svg { - height: 18px; - width: 18px; - } + .ci-status-icon-failed { + background-color: rgba($gl-danger, .1); + } - .ci-status-text { - max-width: 95px; - padding-bottom: 3px; - position: relative; - top: 3px; - } + .ci-status-icon-pending, + .ci-status-icon-success_with_warnings { + background-color: rgba($gl-warning, .1); } - } -} -// Action Icons -.ci-action-icon-container .ci-action-icon-wrapper { - i { - color: $border-color; - border-radius: 100%; - border: 1px solid $border-color; - padding: 5px 6px; - font-size: 13px; - background: $white-light; - height: 30px; - width: 30px; + .ci-status-icon-running { + background-color: rgba($blue-normal, .1); + } - &::before { - position: relative; - top: 3px; - left: 3px; + .ci-status-icon-canceled, + .ci-status-icon-disabled, + .ci-status-icon-not-found { + background-color: rgba($gl-gray, .1); } - &:hover { - color: $gl-text-color; - background-color: $stage-hover-bg; - border: 1px solid $stage-hover-bg; + .ci-status-icon-created, + .ci-status-icon-skipped { + background-color: rgba($gray-darkest, .1); } } - .ci-play-icon { - padding: 5px 5px 5px 7px; + .mini-pipeline-graph-icon-container { + .ci-status-icon:hover, + .ci-status-icon:focus { + width: 28px; + padding: 0 8px 0 0; + + + .dropdown-caret { + display: inline-block; + } + } + + .dropdown-caret { + font-size: 11px; + position: relative; + top: 3px; + left: -11px; + margin-right: -6px; + display: none; + z-index: 2; + } } } @@ -745,4 +889,4 @@ min-height: 450px; } } -} +}
\ No newline at end of file diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index b83c3a872cf..efe9c001bcf 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -82,6 +82,8 @@ class GroupsController < Groups::ApplicationController if Groups::UpdateService.new(@group, current_user, group_params).execute redirect_to edit_group_path(@group), notice: "Group '#{@group.name}' was successfully updated." else + @group.reset_path! + render action: "edit" end end diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index 85188cfdd4c..cc347922c6a 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -8,6 +8,7 @@ class Projects::PipelinesController < Projects::ApplicationController def index @scope = params[:scope] @pipelines = PipelinesFinder.new(project).execute(scope: @scope).page(params[:page]).per(30) + @pipelines = @pipelines.includes(project: :namespace) @running_or_pending_count = PipelinesFinder.new(project).execute(scope: 'running').count @pipelines_count = PipelinesFinder.new(project).execute.count @@ -40,6 +41,15 @@ class Projects::PipelinesController < Projects::ApplicationController end end + def stage + @stage = pipeline.stage(params[:stage]) + return not_found unless @stage + + respond_to do |format| + format.json { render json: { html: view_to_html_string('projects/pipelines/_stage') } } + end + end + def retry pipeline.retry_failed(current_user) diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb index 92bac149313..1ee6c1d3afa 100644 --- a/app/helpers/auth_helper.rb +++ b/app/helpers/auth_helper.rb @@ -1,5 +1,5 @@ module AuthHelper - PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2 facebook azure_oauth2).freeze + PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2 facebook azure_oauth2 authentiq).freeze FORM_BASED_PROVIDERS = [/\Aldap/, 'crowd'].freeze def ldap_enabled? diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index 66a720a9426..e9461b9f859 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -128,50 +128,11 @@ module CommitsHelper end def revert_commit_link(commit, continue_to_path, btn_class: nil, has_tooltip: true) - return unless current_user - - tooltip = "Revert this #{commit.change_type_title(current_user)} in a new merge request" if has_tooltip - - if can_collaborate_with_project? - btn_class = "btn btn-warning btn-#{btn_class}" unless btn_class.nil? - link_to 'Revert', '#modal-revert-commit', 'data-toggle' => 'modal', 'data-container' => 'body', title: (tooltip if has_tooltip), class: "#{btn_class} #{'has-tooltip' if has_tooltip}" - elsif can?(current_user, :fork_project, @project) - continue_params = { - to: continue_to_path, - notice: edit_in_new_fork_notice + ' Try to revert this commit again.', - notice_now: edit_in_new_fork_notice_now - } - fork_path = namespace_project_forks_path(@project.namespace, @project, - namespace_key: current_user.namespace.id, - continue: continue_params) - - btn_class = "btn btn-grouped btn-warning" unless btn_class.nil? - - link_to 'Revert', fork_path, class: btn_class, method: :post, 'data-toggle' => 'tooltip', 'data-container' => 'body', title: (tooltip if has_tooltip) - end + commit_action_link('revert', commit, continue_to_path, btn_class: btn_class, has_tooltip: has_tooltip) end def cherry_pick_commit_link(commit, continue_to_path, btn_class: nil, has_tooltip: true) - return unless current_user - - tooltip = "Cherry-pick this #{commit.change_type_title(current_user)} in a new merge request" - - if can_collaborate_with_project? - btn_class = "btn btn-default btn-#{btn_class}" unless btn_class.nil? - link_to 'Cherry-pick', '#modal-cherry-pick-commit', 'data-toggle' => 'modal', 'data-container' => 'body', title: (tooltip if has_tooltip), class: "#{btn_class} #{'has-tooltip' if has_tooltip}" - elsif can?(current_user, :fork_project, @project) - continue_params = { - to: continue_to_path, - notice: edit_in_new_fork_notice + ' Try to cherry-pick this commit again.', - notice_now: edit_in_new_fork_notice_now - } - fork_path = namespace_project_forks_path(@project.namespace, @project, - namespace_key: current_user.namespace.id, - continue: continue_params) - - btn_class = "btn btn-grouped btn-close" unless btn_class.nil? - link_to 'Cherry-pick', fork_path, class: "#{btn_class}", method: :post, 'data-toggle' => 'tooltip', 'data-container' => 'body', title: (tooltip if has_tooltip) - end + commit_action_link('cherry-pick', commit, continue_to_path, btn_class: btn_class, has_tooltip: has_tooltip) end protected @@ -211,6 +172,28 @@ module CommitsHelper end end + def commit_action_link(action, commit, continue_to_path, btn_class: nil, has_tooltip: true) + return unless current_user + + tooltip = "#{action.capitalize} this #{commit.change_type_title(current_user)} in a new merge request" if has_tooltip + btn_class = "btn btn-#{btn_class}" unless btn_class.nil? + + if can_collaborate_with_project? + link_to action.capitalize, "#modal-#{action}-commit", 'data-toggle' => 'modal', 'data-container' => 'body', title: (tooltip if has_tooltip), class: "#{btn_class} #{'has-tooltip' if has_tooltip}" + elsif can?(current_user, :fork_project, @project) + continue_params = { + to: continue_to_path, + notice: "#{edit_in_new_fork_notice} Try to #{action} this commit again.", + notice_now: edit_in_new_fork_notice_now + } + fork_path = namespace_project_forks_path(@project.namespace, @project, + namespace_key: current_user.namespace.id, + continue: continue_params) + + link_to action.capitalize, fork_path, class: btn_class, method: :post, 'data-toggle' => 'tooltip', 'data-container' => 'body', title: (tooltip if has_tooltip) + end + end + def view_file_btn(commit_sha, diff_new_path, project) link_to( namespace_project_blob_path(project.namespace, project, diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 48354cdbefb..f2f6453b3b9 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -116,6 +116,11 @@ module Ci where.not(duration: nil).sum(:duration) end + def stage(name) + stage = Ci::Stage.new(self, name: name) + stage unless stage.statuses_count.zero? + end + def stages_count statuses.select(:stage).distinct.count end diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb index 7ef59445d77..d035eda6df5 100644 --- a/app/models/ci/stage.rb +++ b/app/models/ci/stage.rb @@ -18,6 +18,10 @@ module Ci name end + def statuses_count + @statuses_count ||= statuses.count + end + def status @status ||= statuses.latest.status end diff --git a/app/models/issue.rb b/app/models/issue.rb index 738c96e4db3..6825553512f 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -39,6 +39,8 @@ class Issue < ActiveRecord::Base scope :created_after, -> (datetime) { where("created_at >= ?", datetime) } + scope :include_associations, -> { includes(:assignee, :labels, project: :namespace) } + attr_spammable :title, spam_title: true attr_spammable :description, spam_description: true diff --git a/app/models/namespace.rb b/app/models/namespace.rb index fd42f2328d8..b52f08c7081 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -98,7 +98,7 @@ class Namespace < ActiveRecord::Base def move_dir if any_project_has_container_registry_tags? - raise Exception.new('Namespace cannot be moved, because at least one project has tags in container registry') + raise Gitlab::UpdatePathError.new('Namespace cannot be moved, because at least one project has tags in container registry') end # Move the namespace directory in all storages paths used by member projects @@ -111,7 +111,7 @@ class Namespace < ActiveRecord::Base # if we cannot move namespace directory we should rollback # db changes in order to prevent out of sync between db and fs - raise Exception.new('namespace directory cannot be moved') + raise Gitlab::UpdatePathError.new('namespace directory cannot be moved') end end diff --git a/app/models/project.rb b/app/models/project.rb index 72d3da64f2d..26fa20f856d 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -94,9 +94,9 @@ class Project < ActiveRecord::Base has_one :asana_service, dependent: :destroy has_one :gemnasium_service, dependent: :destroy has_one :mattermost_slash_commands_service, dependent: :destroy - has_one :mattermost_notification_service, dependent: :destroy + has_one :mattermost_service, dependent: :destroy has_one :slack_slash_commands_service, dependent: :destroy - has_one :slack_notification_service, dependent: :destroy + has_one :slack_service, dependent: :destroy has_one :buildkite_service, dependent: :destroy has_one :bamboo_service, dependent: :destroy has_one :teamcity_service, dependent: :destroy diff --git a/app/models/project_services/mattermost_notification_service.rb b/app/models/project_services/mattermost_service.rb index de18c4b1f00..0650f930402 100644 --- a/app/models/project_services/mattermost_notification_service.rb +++ b/app/models/project_services/mattermost_service.rb @@ -1,4 +1,4 @@ -class MattermostNotificationService < ChatNotificationService +class MattermostService < ChatNotificationService def title 'Mattermost notifications' end @@ -8,7 +8,7 @@ class MattermostNotificationService < ChatNotificationService end def to_param - 'mattermost_notification' + 'mattermost' end def help diff --git a/app/models/project_services/slack_notification_service.rb b/app/models/project_services/slack_service.rb index 3cbf89efba4..0583470d3b5 100644 --- a/app/models/project_services/slack_notification_service.rb +++ b/app/models/project_services/slack_service.rb @@ -1,4 +1,4 @@ -class SlackNotificationService < ChatNotificationService +class SlackService < ChatNotificationService def title 'Slack notifications' end @@ -8,7 +8,7 @@ class SlackNotificationService < ChatNotificationService end def to_param - 'slack_notification' + 'slack' end def help diff --git a/app/models/service.rb b/app/models/service.rb index 8abd8e73e43..19ef3ba9c23 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -216,13 +216,13 @@ class Service < ActiveRecord::Base jira kubernetes mattermost_slash_commands - mattermost_notification + mattermost pipelines_email pivotaltracker pushover redmine slack_slash_commands - slack_notification + slack teamcity ] end diff --git a/app/services/groups/update_service.rb b/app/services/groups/update_service.rb index fff2273f402..4e878ec556a 100644 --- a/app/services/groups/update_service.rb +++ b/app/services/groups/update_service.rb @@ -14,7 +14,13 @@ module Groups group.assign_attributes(params) - group.save + begin + group.save + rescue Gitlab::UpdatePathError => e + group.errors.add(:base, e.message) + + false + end end end end diff --git a/app/validators/project_path_validator.rb b/app/validators/project_path_validator.rb index 927c67b65b0..d9ab8f167d8 100644 --- a/app/validators/project_path_validator.rb +++ b/app/validators/project_path_validator.rb @@ -14,7 +14,8 @@ class ProjectPathValidator < ActiveModel::EachValidator # without tree as reserved name routing can match 'group/project' as group name, # 'tree' as project name and 'deploy_keys' as route. # - RESERVED = (NamespaceValidator::RESERVED + + RESERVED = (NamespaceValidator::RESERVED - + %w[dashboard] + %w[tree commits wikis new edit create update logs_tree preview blob blame raw files create_dir find_file]).freeze diff --git a/app/views/ci/status/_graph_badge.html.haml b/app/views/ci/status/_graph_badge.html.haml index 52b4d77d074..dd2f649de9a 100644 --- a/app/views/ci/status/_graph_badge.html.haml +++ b/app/views/ci/status/_graph_badge.html.haml @@ -3,18 +3,18 @@ - subject = local_assigns.fetch(:subject) - status = subject.detailed_status(current_user) - klass = "ci-status-icon ci-status-icon-#{status.group}" +- tooltip = "#{subject.name} - #{status.label}" - if status.has_details? - = link_to status.details_path, class: 'build-content' do + = link_to status.details_path, class: 'build-content has-tooltip', data: { toggle: 'tooltip', title: tooltip } do %span{ class: klass }= custom_icon(status.icon) - .ci-status-text{ 'data-toggle' => 'tooltip', 'data-title' => "#{subject.name} - #{status.label}" }= subject.name + .ci-status-text= subject.name - else - .build-content + .build-content.has-tooltip{ data: { toggle: 'tooltip', title: tooltip } } %span{ class: klass }= custom_icon(status.icon) - .ci-status-text{ 'data-toggle' => 'tooltip', 'data-title' => "#{subject.name} - #{status.label}" }= subject.name + .ci-status-text= subject.name - if status.has_action? - = link_to status.action_path, method: status.action_method, - title: status.action_title, class: 'ci-action-icon-container' do + = link_to status.action_path, class: 'ci-action-icon-container has-tooltip', method: status.action_method, data: { toggle: 'tooltip', title: status.action_title } do %i.ci-action-icon-wrapper = icon(status.action_icon, class: status.action_class) diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml index ac04f57e217..b69114c96cc 100644 --- a/app/views/layouts/nav/_admin.html.haml +++ b/app/views/layouts/nav/_admin.html.haml @@ -31,7 +31,7 @@ = link_to admin_abuse_reports_path, title: "Abuse Reports" do %span Abuse Reports - %span.badge.count= number_with_delimiter(AbuseReport.count(:all)) + %span.badge.badge-dark.count= number_with_delimiter(AbuseReport.count(:all)) - if askimet_enabled? = nav_link(controller: :spam_logs) do diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml index f3539fd372d..221f3ec1ffe 100644 --- a/app/views/layouts/nav/_group.html.haml +++ b/app/views/layouts/nav/_group.html.haml @@ -26,13 +26,13 @@ %span Issues - issues = IssuesFinder.new(current_user, group_id: @group.id, state: 'opened').execute - %span.badge.count= number_with_delimiter(issues.count) + %span.badge.badge-dark.count= number_with_delimiter(issues.count) = nav_link(path: 'groups#merge_requests') do = link_to merge_requests_group_path(@group), title: 'Merge Requests' do %span Merge Requests - merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened', non_archived: true).execute - %span.badge.count= number_with_delimiter(merge_requests.count) + %span.badge.badge-dark.count= number_with_delimiter(merge_requests.count) = nav_link(controller: [:group_members]) do = link_to group_group_members_path(@group), title: 'Members' do %span diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index 904d11c2cf4..cc1571cbb4f 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -61,14 +61,14 @@ %span Issues - if @project.default_issues_tracker? - %span.badge.count.issue_counter= number_with_delimiter(IssuesFinder.new(current_user, project_id: @project.id).execute.opened.count) + %span.badge.badge-dark.count.issue_counter= number_with_delimiter(IssuesFinder.new(current_user, project_id: @project.id).execute.opened.count) - if project_nav_tab? :merge_requests = nav_link(controller: :merge_requests) do = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do %span Merge Requests - %span.badge.count.merge_counter= number_with_delimiter(MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened.count) + %span.badge.badge-dark.count.merge_counter= number_with_delimiter(MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened.count) - if project_nav_tab? :wiki = nav_link(controller: :wikis) do diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml index 3f05a21990f..2f8f153f9a9 100644 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -43,10 +43,25 @@ %td.stage-cell - pipeline.stages.each do |stage| - if stage.status - - tooltip = "#{stage.name.titleize}: #{stage.status || 'not found'}" - .stage-container - = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id, anchor: stage.name), class: "has-tooltip ci-status-icon-#{stage.status}", title: tooltip do - = ci_icon_for_status(stage.status) + - detailed_status = stage.detailed_status(current_user) + - icon_status = "#{detailed_status.icon}_borderless" + - status_klass = "ci-status-icon ci-status-icon-#{detailed_status.group}" + + .stage-container.mini-pipeline-graph + .dropdown.inline.build-content + %button.has-tooltip.builds-dropdown.js-builds-dropdown-button{ type: 'button', data: { toggle: 'dropdown', title: "#{stage.name}: #{detailed_status.label}", placement: 'top', "stage-endpoint" => stage_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline, stage: stage.name)}} + %span.has-tooltip{ class: status_klass } + %span.mini-pipeline-graph-icon-container + %span{ class: status_klass }= custom_icon(icon_status) + = icon('caret-down', class: 'dropdown-caret') + + .js-builds-dropdown-container + .dropdown-menu.grouped-pipeline-dropdown + .arrow-up + .js-builds-dropdown-list + + .js-builds-dropdown-loading.builds-dropdown-loading.hidden + %span.fa.fa-spinner.fa-spin %td - if pipeline.duration @@ -66,7 +81,7 @@ .btn-group.inline - if actions.any? .btn-group - %a.dropdown-toggle.btn.btn-default{type: 'button', 'data-toggle' => 'dropdown'} + %a.dropdown-toggle.btn.btn-default.js-pipeline-dropdown-manual-actions{type: 'button', 'data-toggle' => 'dropdown'} = custom_icon('icon_play') = icon('caret-down') %ul.dropdown-menu.dropdown-menu-align-right @@ -77,7 +92,7 @@ %span= build.name.humanize - if artifacts.present? .btn-group - %a.dropdown-toggle.btn.btn-default.build-artifacts{type: 'button', 'data-toggle' => 'dropdown'} + %a.dropdown-toggle.btn.btn-default.build-artifacts.js-pipeline-dropdown-download{type: 'button', 'data-toggle' => 'dropdown'} = icon("download") = icon('caret-down') %ul.dropdown-menu.dropdown-menu-align-right diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml index 7f42fde0fea..5a9f7295135 100644 --- a/app/views/projects/commit/_pipelines_list.haml +++ b/app/views/projects/commit/_pipelines_list.haml @@ -4,12 +4,12 @@ .nothing-here-block No pipelines to show - else .table-holder - %table.table.ci-table - %tbody - %th Status - %th Pipeline - %th Commit - %th Stages - %th - %th + %table.table.ci-table.js-pipeline-table + %thead + %th.pipeline-status Status + %th.pipeline-info Pipeline + %th.pipeline-commit Commit + %th.pipeline-stages Stages + %th.pipeline-date + %th.pipeline-actions = render pipelines, commit_sha: true, stage: true, allow_retry: true, show_commit: false diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml index 9ab7971b56c..5bc417d1760 100644 --- a/app/views/projects/merge_requests/widget/_heading.html.haml +++ b/app/views/projects/merge_requests/widget/_heading.html.haml @@ -17,7 +17,7 @@ - # TODO, remove in later versions when services like Jenkins will set CI status via Commit status API .mr-widget-heading - %w[success skipped canceled failed running pending].each do |status| - .ci_widget{class: "ci-#{status}", style: "display:none"} + .ci_widget{class: "ci-#{status} ci-status-icon-#{status}", style: "display:none"} = ci_icon_for_status(status) %span CI build diff --git a/app/views/projects/merge_requests/widget/_merged_buttons.haml b/app/views/projects/merge_requests/widget/_merged_buttons.haml index d836a253507..9eef011b591 100644 --- a/app/views/projects/merge_requests/widget/_merged_buttons.haml +++ b/app/views/projects/merge_requests/widget/_merged_buttons.haml @@ -5,10 +5,10 @@ - if can_remove_source_branch || mr_can_be_reverted || mr_can_be_cherry_picked .clearfix.merged-buttons - if can_remove_source_branch - = link_to namespace_project_branch_path(@merge_request.source_project.namespace, @merge_request.source_project, @merge_request.source_branch), remote: true, method: :delete, class: "btn btn-default btn-sm remove_source_branch" do + = link_to namespace_project_branch_path(@merge_request.source_project.namespace, @merge_request.source_project, @merge_request.source_branch), remote: true, method: :delete, class: "btn btn-default remove_source_branch" do = icon('trash-o') Remove Source Branch - if mr_can_be_reverted - = revert_commit_link(@merge_request.merge_commit, namespace_project_merge_request_path(@project.namespace, @project, @merge_request), btn_class: 'sm') + = revert_commit_link(@merge_request.merge_commit, namespace_project_merge_request_path(@project.namespace, @project, @merge_request), btn_class: "warning") - if mr_can_be_cherry_picked - = cherry_pick_commit_link(@merge_request.merge_commit, namespace_project_merge_request_path(@project.namespace, @project, @merge_request), btn_class: 'sm') + = cherry_pick_commit_link(@merge_request.merge_commit, namespace_project_merge_request_path(@project.namespace, @project, @merge_request), btn_class: "default") diff --git a/app/views/projects/pipelines/_stage.html.haml b/app/views/projects/pipelines/_stage.html.haml new file mode 100644 index 00000000000..20456e792e7 --- /dev/null +++ b/app/views/projects/pipelines/_stage.html.haml @@ -0,0 +1,4 @@ +%ul + - @stage.statuses.each do |status| + %li.dropdown-build + = render 'ci/status/graph_badge', subject: status diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml index 030cd8ef78f..28026ccf861 100644 --- a/app/views/projects/pipelines/index.html.haml +++ b/app/views/projects/pipelines/index.html.haml @@ -42,14 +42,14 @@ .nothing-here-block No pipelines to show - else .table-holder - %table.table.ci-table + %table.table.ci-table.js-pipeline-table %thead - %th Status - %th Pipeline - %th Commit - %th Stages - %th - %th.hidden-xs + %th.pipeline-status Status + %th.pipeline-info Pipeline + %th.pipeline-commit Commit + %th.pipeline-stages Stages + %th.pipeline-date + %th.pipeline-actions.hidden-xs = render @pipelines, commit_sha: true, stage: true, allow_retry: true = paginate @pipelines, theme: 'gitlab' diff --git a/app/views/shared/empty_states/_issues.html.haml b/app/views/shared/empty_states/_issues.html.haml index 07d4927b6c9..e2033654018 100644 --- a/app/views/shared/empty_states/_issues.html.haml +++ b/app/views/shared/empty_states/_issues.html.haml @@ -10,10 +10,10 @@ .text-content - if has_button && current_user %h4 - The Issue Tracker is a good place to add things that need to be improved or solved in a project! + The Issue Tracker is the place to add things that need to be improved or solved in a project %p - An issue can be a bug, a todo or a feature request that needs to be discussed in a project. - Besides, issues are searchable and filterable. + Issues can be bugs, tasks or ideas to be discussed. + Also, issues are searchable and filterable. - if project_select_button = render 'shared/new_project_item_select', path: 'issues/new', label: 'New issue' - else diff --git a/app/views/shared/icons/_icon_status_canceled_borderless.svg b/app/views/shared/icons/_icon_status_canceled_borderless.svg new file mode 100644 index 00000000000..bf7fb29185f --- /dev/null +++ b/app/views/shared/icons/_icon_status_canceled_borderless.svg @@ -0,0 +1 @@ +<svg width="22px" height="22px" viewBox="0 0 22 22" xmlns="http://www.w3.org/2000/svg"><path d="M8.17142857,5.97142857 L15.8714286,13.6714286 C16.1857143,13.9857143 16.1857143,14.4571429 15.8714286,14.7714286 L14.7714286,15.8714286 C14.4571429,16.1857143 13.9857143,16.1857143 13.6714286,15.8714286 L5.97142857,8.17142857 C5.65714286,7.85714286 5.65714286,7.38571429 5.97142857,7.07142857 L7.07142857,5.97142857 C7.38571429,5.65714286 7.85714286,5.65714286 8.17142857,5.97142857" id="Shape"></path></svg> diff --git a/app/views/shared/icons/_icon_status_created_borderless.svg b/app/views/shared/icons/_icon_status_created_borderless.svg new file mode 100644 index 00000000000..1810d023be8 --- /dev/null +++ b/app/views/shared/icons/_icon_status_created_borderless.svg @@ -0,0 +1 @@ +<svg width="22px" height="22px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg"><circle id="Oval" cx="11" cy="11" r="5.10714286"></circle></svg> diff --git a/app/views/shared/icons/_icon_status_failed_borderless.svg b/app/views/shared/icons/_icon_status_failed_borderless.svg new file mode 100644 index 00000000000..b7022350c74 --- /dev/null +++ b/app/views/shared/icons/_icon_status_failed_borderless.svg @@ -0,0 +1 @@ +<svg width="22px" height="22px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M12.1458333,9.85416667 L12.1458333,6.74047388 C12.1458333,6.4826434 11.9382041,6.28571429 11.6820804,6.28571429 L10.3179196,6.28571429 C10.0656535,6.28571429 9.85416667,6.48931709 9.85416667,6.74047388 L9.85416667,9.85416667 L6.74047388,9.85416667 C6.4826434,9.85416667 6.28571429,10.0617959 6.28571429,10.3179196 L6.28571429,11.6820804 C6.28571429,11.9343465 6.48931709,12.1458333 6.74047388,12.1458333 L9.85416667,12.1458333 L9.85416667,15.2595261 C9.85416667,15.5173566 10.0617959,15.7142857 10.3179196,15.7142857 L11.6820804,15.7142857 C11.9343465,15.7142857 12.1458333,15.5106829 12.1458333,15.2595261 L12.1458333,12.1458333 L15.2595261,12.1458333 C15.5173566,12.1458333 15.7142857,11.9382041 15.7142857,11.6820804 L15.7142857,10.3179196 C15.7142857,10.0656535 15.5106829,9.85416667 15.2595261,9.85416667 L12.1458333,9.85416667 Z" id="Combined-Shape" transform="translate(11.000000, 11.000000) rotate(-45.000000) translate(-11.000000, -11.000000) "></path></svg> diff --git a/app/views/shared/icons/_icon_status_manual_borderless.svg b/app/views/shared/icons/_icon_status_manual_borderless.svg new file mode 100644 index 00000000000..5eec665688b --- /dev/null +++ b/app/views/shared/icons/_icon_status_manual_borderless.svg @@ -0,0 +1 @@ +<svg width="22px" height="22px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M16.5,11.9906832 L16.5,10.0093168 L15.2625,9.80434783 C15.19375,9.5310559 15.05625,9.25776398 14.85,8.84782609 L15.60625,7.82298137 L14.1625,6.38819876 L13.13125,7.13975155 C12.7875,6.93478261 12.44375,6.79813665 12.16875,6.72981366 L12.03125,5.5 L10.0375,5.5 L9.83125,6.72981366 C9.4875,6.79813665 9.2125,6.93478261 8.86875,7.13975155 L7.8375,6.38819876 L6.39375,7.82298137 L7.08125,8.84782609 C6.875,9.18944099 6.80625,9.46273292 6.66875,9.80434783 L5.5,9.94099379 L5.5,11.9223602 L6.7375,12.1273292 C6.80625,12.4689441 6.94375,12.742236 7.15,13.0838509 L6.4625,14.1086957 L7.90625,15.5434783 L8.9375,14.8602484 C9.2125,14.9968944 9.55625,15.1335404 9.9,15.2701863 L10.10625,16.5 L12.16875,16.5 L12.375,15.2701863 C12.71875,15.2018634 12.99375,15.0652174 13.3375,14.8602484 L14.36875,15.6118012 L15.8125,14.1770186 L15.05625,13.1521739 C15.2625,12.810559 15.4,12.4689441 15.46875,12.1956522 L16.5,11.9906832 L16.5,11.9906832 Z M11,13.015528 C9.83125,13.015528 8.9375,12.1273292 8.9375,10.9658385 C8.9375,9.80434783 9.83125,8.91614907 11,8.91614907 C12.16875,8.91614907 13.0625,9.80434783 13.0625,10.9658385 C13.0625,12.1273292 12.16875,13.015528 11,13.015528 L11,13.015528 Z" id="Shape" ></path></svg> diff --git a/app/views/shared/icons/_icon_status_pending_borderless.svg b/app/views/shared/icons/_icon_status_pending_borderless.svg new file mode 100644 index 00000000000..8d66e9e6c9c --- /dev/null +++ b/app/views/shared/icons/_icon_status_pending_borderless.svg @@ -0,0 +1 @@ +<svg width="22px" height="22px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M7.38571429,8.32857143 C7.38571429,8.01428571 7.54285714,7.85714286 7.85714286,7.85714286 L9.27142857,7.85714286 C9.58571429,7.85714286 9.74285714,8.01428571 9.74285714,8.32857143 L9.74285714,13.6714286 C9.74285714,13.9857143 9.58571429,14.1428571 9.27142857,14.1428571 L7.85714286,14.1428571 C7.54285714,14.1428571 7.38571429,13.9857143 7.38571429,13.6714286 L7.38571429,8.32857143 M12.1,8.32857143 C12.1,8.01428571 12.2571429,7.85714286 12.5714286,7.85714286 L13.9857143,7.85714286 C14.3,7.85714286 14.4571429,8.01428571 14.4571429,8.32857143 L14.4571429,13.6714286 C14.4571429,13.9857143 14.3,14.1428571 13.9857143,14.1428571 L12.5714286,14.1428571 C12.2571429,14.1428571 12.1,13.9857143 12.1,13.6714286 L12.1,8.32857143" id="Shape"></path></svg> diff --git a/app/views/shared/icons/_icon_status_running_borderless.svg b/app/views/shared/icons/_icon_status_running_borderless.svg new file mode 100644 index 00000000000..2757a168ed5 --- /dev/null +++ b/app/views/shared/icons/_icon_status_running_borderless.svg @@ -0,0 +1 @@ +<svg width="22px" height="22px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M11,4.71428571 C14.4571429,4.71428571 17.2857143,7.54285714 17.2857143,11 C17.2857143,14.4571429 14.4571429,17.2857143 11,17.2857143 C8.95714286,17.2857143 7.07142857,16.1857143 5.81428571,14.6142857 L11,11 L11,4.71428571" id="Shape"></path></svg> diff --git a/app/views/shared/icons/_icon_status_skipped_borderless.svg b/app/views/shared/icons/_icon_status_skipped_borderless.svg new file mode 100644 index 00000000000..fb3e930b3cb --- /dev/null +++ b/app/views/shared/icons/_icon_status_skipped_borderless.svg @@ -0,0 +1 @@ +<svg width="22px" height="22px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M12.0846,12.1 L10.6623,13.5223 C10.2454306,13.9539168 10.2513924,14.6399933 10.6756996,15.0643004 C11.1000067,15.4886076 11.7860832,15.4945694 12.2177,15.0777 L15.1261,12.1693 C15.7708612,11.5230891 15.7708612,10.4769109 15.1261,9.8307 L12.2177,6.9223 C11.7860832,6.50543057 11.1000067,6.51139239 10.6756996,6.93569957 C10.2513924,7.36000675 10.2454306,8.04608322 10.6623,8.4777 L12.0846,9.9 L7.04,9.9 C6.43248678,9.9 5.94,10.3924868 5.94,11 C5.94,11.6075132 6.43248678,12.1 7.04,12.1 L12.0846,12.1 L12.0846,12.1 Z" id="Shape"></path></svg> diff --git a/app/views/shared/icons/_icon_status_success_borderless.svg b/app/views/shared/icons/_icon_status_success_borderless.svg new file mode 100644 index 00000000000..8ee5be7ab78 --- /dev/null +++ b/app/views/shared/icons/_icon_status_success_borderless.svg @@ -0,0 +1 @@ +<svg width="22px" height="22px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M11.4583333,12.375 L8.70008808,12.375 C8.45889044,12.375 8.25,12.5826293 8.25,12.8387529 L8.25,14.2029137 C8.25,14.4551799 8.4515113,14.6666667 8.70008808,14.6666667 L12.9619841,14.6666667 C13.3891296,14.6666667 13.75,14.3193051 13.75,13.8908129 L13.75,13.2899463 L13.75,6.42552703 C13.75,6.16226705 13.5423707,5.95833333 13.2862471,5.95833333 L11.9220863,5.95833333 C11.6698201,5.95833333 11.4583333,6.16750307 11.4583333,6.42552703 L11.4583333,12.375 Z" id="Combined-Shape" transform="translate(11.000000, 10.312500) rotate(-315.000000) translate(-11.000000, -10.312500) "></path></svg> diff --git a/app/views/shared/icons/_icon_status_warning_borderless.svg b/app/views/shared/icons/_icon_status_warning_borderless.svg new file mode 100644 index 00000000000..7b061624521 --- /dev/null +++ b/app/views/shared/icons/_icon_status_warning_borderless.svg @@ -0,0 +1 @@ +<svg width="22px" height="22px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M9.42857143,5.5 C9.42857143,5.02857143 9.74285714,4.71428571 10.2142857,4.71428571 L11.7857143,4.71428571 C12.2571429,4.71428571 12.5714286,5.02857143 12.5714286,5.5 L12.5714286,11.7857143 C12.5714286,12.2571429 12.2571429,12.5714286 11.7857143,12.5714286 L10.2142857,12.5714286 C9.74285714,12.5714286 9.42857143,12.2571429 9.42857143,11.7857143 L9.42857143,5.5 M9.42857143,14.9285714 C9.42857143,14.4571429 9.74285714,14.1428571 10.2142857,14.1428571 L11.7857143,14.1428571 C12.2571429,14.1428571 12.5714286,14.4571429 12.5714286,14.9285714 L12.5714286,16.5 C12.5714286,16.9714286 12.2571429,17.2857143 11.7857143,17.2857143 L10.2142857,17.2857143 C9.74285714,17.2857143 9.42857143,16.9714286 9.42857143,16.5 L9.42857143,14.9285714" id="Shape"></path></svg> diff --git a/app/views/shared/issuable/_milestone_dropdown.html.haml b/app/views/shared/issuable/_milestone_dropdown.html.haml index 40fe53e6a8d..415361f8fbf 100644 --- a/app/views/shared/issuable/_milestone_dropdown.html.haml +++ b/app/views/shared/issuable/_milestone_dropdown.html.haml @@ -3,7 +3,7 @@ - show_menu_above = show_menu_above || false - selected_text = selected.try(:title) || params[:milestone_title] - dropdown_title = local_assigns.fetch(:dropdown_title, "Filter by milestone") -- if selected.present? +- if selected.present? || params[:milestone_title].present? = hidden_field_tag(name, name == :milestone_title ? selected_text : selected.id) = dropdown_tag(milestone_dropdown_label(selected_text), options: { title: dropdown_title, toggle_class: "js-milestone-select js-filter-submit #{extra_class}", filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", footer_content: project.present?, data: { show_no: true, show_menu_above: show_menu_above, show_any: show_any, show_upcoming: show_upcoming, field_name: name, selected: selected.try(:title), project_id: project.try(:id), milestones: milestones_filter_dropdown_path, default_label: "Milestone" } }) do diff --git a/app/views/shared/milestones/_tabs.html.haml b/app/views/shared/milestones/_tabs.html.haml index 2b6ce2d7e7a..c8f2319d95a 100644 --- a/app/views/shared/milestones/_tabs.html.haml +++ b/app/views/shared/milestones/_tabs.html.haml @@ -21,7 +21,7 @@ .tab-content.milestone-content .tab-pane.active#tab-issues - = render 'shared/milestones/issues_tab', issues: milestone.issues_visible_to_user(current_user), show_project_name: show_project_name, show_full_project_name: show_full_project_name + = render 'shared/milestones/issues_tab', issues: milestone.issues_visible_to_user(current_user).include_associations, show_project_name: show_project_name, show_full_project_name: show_full_project_name .tab-pane#tab-merge-requests = render 'shared/milestones/merge_requests_tab', merge_requests: milestone.merge_requests, show_project_name: show_project_name, show_full_project_name: show_full_project_name .tab-pane#tab-participants diff --git a/app/views/u2f/_authenticate.html.haml b/app/views/u2f/_authenticate.html.haml index 232ca26c1af..fa998c91f72 100644 --- a/app/views/u2f/_authenticate.html.haml +++ b/app/views/u2f/_authenticate.html.haml @@ -13,7 +13,7 @@ %script#js-authenticate-u2f-error{ type: "text/template" } %div - %p <%= error_message %> + %p <%= error_message %> (error code: <%= error_code %>) %a.btn.btn-warning#js-u2f-try-again Try again? %script#js-authenticate-u2f-authenticated{ type: "text/template" } diff --git a/app/views/u2f/_register.html.haml b/app/views/u2f/_register.html.haml index 8f7b42eb351..fcc33f04237 100644 --- a/app/views/u2f/_register.html.haml +++ b/app/views/u2f/_register.html.haml @@ -23,7 +23,7 @@ %script#js-register-u2f-error{ type: "text/template" } %div %p - %span <%= error_message %> + %span <%= error_message %> (error code: <%= error_code %>) %a.btn.btn-warning#js-u2f-try-again Try again? %script#js-register-u2f-registered{ type: "text/template" } diff --git a/changelogs/unreleased/19703-direct-link-pipelines.yml b/changelogs/unreleased/19703-direct-link-pipelines.yml new file mode 100644 index 00000000000..d846ad41e0f --- /dev/null +++ b/changelogs/unreleased/19703-direct-link-pipelines.yml @@ -0,0 +1,4 @@ +--- +title: Adds Direct link from pipeline list to builds +merge_request: 8097 +author: diff --git a/changelogs/unreleased/25895-fix-headers-in-ci-api-helpers.yml b/changelogs/unreleased/25895-fix-headers-in-ci-api-helpers.yml new file mode 100644 index 00000000000..b9a8e17c64a --- /dev/null +++ b/changelogs/unreleased/25895-fix-headers-in-ci-api-helpers.yml @@ -0,0 +1,4 @@ +--- +title: Ensure nil User-Agent doesn't break the CI API +merge_request: +author: diff --git a/changelogs/unreleased/25898-ci-icon-color-mr.yml b/changelogs/unreleased/25898-ci-icon-color-mr.yml new file mode 100644 index 00000000000..dd0f93e176f --- /dev/null +++ b/changelogs/unreleased/25898-ci-icon-color-mr.yml @@ -0,0 +1,4 @@ +--- +title: Adds CSS class to status icon on MR widget to prevent non-colored icon +merge_request: 8219 +author: diff --git a/changelogs/unreleased/8038-authentiq-id-oauth-support.yml b/changelogs/unreleased/8038-authentiq-id-oauth-support.yml new file mode 100644 index 00000000000..36f8ac9c840 --- /dev/null +++ b/changelogs/unreleased/8038-authentiq-id-oauth-support.yml @@ -0,0 +1,4 @@ +--- +title: Add Authentiq as Oauth provider +merge_request: 8038 +author: Alexandros Keramidas diff --git a/changelogs/unreleased/badge-color-on-white-bg.yml b/changelogs/unreleased/badge-color-on-white-bg.yml new file mode 100644 index 00000000000..680d7ff11f0 --- /dev/null +++ b/changelogs/unreleased/badge-color-on-white-bg.yml @@ -0,0 +1,4 @@ +--- +title: Added lighter count badge background-color for on white backgrounds +merge_request: 7873 +author: diff --git a/changelogs/unreleased/dz-rename-invalid-groups.yml b/changelogs/unreleased/dz-rename-invalid-groups.yml new file mode 100644 index 00000000000..90af42da01c --- /dev/null +++ b/changelogs/unreleased/dz-rename-invalid-groups.yml @@ -0,0 +1,4 @@ +--- +title: Rename groups with .git in the end of the path +merge_request: 8199 +author: diff --git a/changelogs/unreleased/dz-whitelist-dashboard-project-path.yml b/changelogs/unreleased/dz-whitelist-dashboard-project-path.yml new file mode 100644 index 00000000000..2787a5c57df --- /dev/null +++ b/changelogs/unreleased/dz-whitelist-dashboard-project-path.yml @@ -0,0 +1,4 @@ +--- +title: Allow projects with 'dashboard' as path +merge_request: +author: diff --git a/changelogs/unreleased/fix-copy-issues-empty-state.yml b/changelogs/unreleased/fix-copy-issues-empty-state.yml new file mode 100644 index 00000000000..a87b7612217 --- /dev/null +++ b/changelogs/unreleased/fix-copy-issues-empty-state.yml @@ -0,0 +1,4 @@ +--- +title: Improve copy in Issue Tracker empty state +merge_request: 8202 +author: diff --git a/changelogs/unreleased/fix-group-path-rename-error.yml b/changelogs/unreleased/fix-group-path-rename-error.yml new file mode 100644 index 00000000000..e3d97ae3987 --- /dev/null +++ b/changelogs/unreleased/fix-group-path-rename-error.yml @@ -0,0 +1,4 @@ +--- +title: Fix 500 error renaming group +merge_request: +author: diff --git a/changelogs/unreleased/fix-import-labels-error.yml b/changelogs/unreleased/fix-import-labels-error.yml new file mode 100644 index 00000000000..86cae3a49ff --- /dev/null +++ b/changelogs/unreleased/fix-import-labels-error.yml @@ -0,0 +1,4 @@ +--- +title: Fix project import label priorities error +merge_request: +author: diff --git a/changelogs/unreleased/jej-fix-n-1-queries-milestones-show.yml b/changelogs/unreleased/jej-fix-n-1-queries-milestones-show.yml new file mode 100644 index 00000000000..ad6eba3faf2 --- /dev/null +++ b/changelogs/unreleased/jej-fix-n-1-queries-milestones-show.yml @@ -0,0 +1,4 @@ +--- +title: Fix N+1 queries on milestone show pages +merge_request: 8185 +author: diff --git a/changelogs/unreleased/remove-u2f-error-logging.yml b/changelogs/unreleased/remove-u2f-error-logging.yml new file mode 100644 index 00000000000..edbe576a976 --- /dev/null +++ b/changelogs/unreleased/remove-u2f-error-logging.yml @@ -0,0 +1,4 @@ +--- +title: Display error code for U2F errors +merge_request: 7305 +author: winniehell diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index f53a3d066df..42e5f105d46 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -368,6 +368,16 @@ production: &base # login_url: '/cas/login', # service_validate_url: '/cas/p3/serviceValidate', # logout_url: '/cas/logout'} } + # - { name: 'authentiq', + # # for client credentials (client ID and secret), go to https://www.authentiq.com/ + # app_id: 'YOUR_CLIENT_ID', + # app_secret: 'YOUR_CLIENT_SECRET', + # args: { + # scope: 'aq:name email~rs address aq:push' + # # redirect_uri parameter is optional except when 'gitlab.host' in this file is set to 'localhost' + # # redirect_uri: 'YOUR_REDIRECT_URI' + # } + # } # - { name: 'github', # app_id: 'YOUR_APP_ID', # app_secret: 'YOUR_APP_SECRET', diff --git a/config/routes/project.rb b/config/routes/project.rb index d8e0243a80e..baabd22b840 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -141,6 +141,7 @@ constraints(ProjectUrlConstrainer.new) do end member do + get :stage post :cancel post :retry get :builds diff --git a/db/migrate/20141006143943_move_slack_service_to_webhook.rb b/db/migrate/20141006143943_move_slack_service_to_webhook.rb index 42e88d6d6e3..561184615cc 100644 --- a/db/migrate/20141006143943_move_slack_service_to_webhook.rb +++ b/db/migrate/20141006143943_move_slack_service_to_webhook.rb @@ -5,7 +5,7 @@ class MoveSlackServiceToWebhook < ActiveRecord::Migration DOWNTIME_REASON = 'Move old fields "token" and "subdomain" to one single field "webhook"' def change - SlackNotificationService.all.each do |slack_service| + SlackService.all.each do |slack_service| if ["token", "subdomain"].all? { |property| slack_service.properties.key? property } token = slack_service.properties['token'] subdomain = slack_service.properties['subdomain'] diff --git a/db/migrate/20161213172958_change_slack_service_to_slack_notification_service.rb b/db/migrate/20161213172958_change_slack_service_to_slack_notification_service.rb index a7278d7b5a6..dc38d0ac906 100644 --- a/db/migrate/20161213172958_change_slack_service_to_slack_notification_service.rb +++ b/db/migrate/20161213172958_change_slack_service_to_slack_notification_service.rb @@ -1,14 +1,11 @@ class ChangeSlackServiceToSlackNotificationService < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers - DOWNTIME = true - DOWNTIME_REASON = 'Rename SlackService to SlackNotificationService' + DOWNTIME = false - def up - execute("UPDATE services SET type = 'SlackNotificationService' WHERE type = 'SlackService'") - end - - def down - execute("UPDATE services SET type = 'SlackService' WHERE type = 'SlackNotificationService'") + # This migration is a no-op, as it existed in an RC but we renamed + # SlackNotificationService back to SlackService: + # https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8191#note_20310845 + def change end end diff --git a/db/migrate/20161220141214_remove_dot_git_from_group_names.rb b/db/migrate/20161220141214_remove_dot_git_from_group_names.rb new file mode 100644 index 00000000000..bd0e4b2cc07 --- /dev/null +++ b/db/migrate/20161220141214_remove_dot_git_from_group_names.rb @@ -0,0 +1,82 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class RemoveDotGitFromGroupNames < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + include Gitlab::ShellAdapter + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + def up + invalid_groups.each do |group| + path_was = group['path'] + path_was_wildcard = quote_string("#{path_was}/%") + path = quote_string(rename_path(path_was)) + + move_namespace(group['id'], path_was, path) + + execute "UPDATE routes SET path = '#{path}' WHERE source_type = 'Namespace' AND source_id = #{group['id']}" + execute "UPDATE namespaces SET path = '#{path}' WHERE id = #{group['id']}" + + select_all("SELECT id, path FROM routes WHERE path LIKE '#{path_was_wildcard}'").each do |route| + new_path = "#{path}/#{route['path'].split('/').last}" + execute "UPDATE routes SET path = '#{new_path}' WHERE id = #{route['id']}" + end + end + end + + def down + # nothing to do here + end + + private + + def invalid_groups + select_all("SELECT id, path FROM namespaces WHERE type = 'Group' AND path LIKE '%.git'") + end + + def route_exists?(path) + select_all("SELECT id, path FROM routes WHERE path = '#{quote_string(path)}'").present? + end + + # Accepts invalid path like test.git and returns test_git or + # test_git1 if test_git already taken + def rename_path(path) + # To stay closer with original name and reduce risk of duplicates + # we rename suffix instead of removing it + path = path.sub(/\.git\z/, '_git') + + counter = 0 + base = path + + while route_exists?(path) + counter += 1 + path = "#{base}#{counter}" + end + + path + end + + def move_namespace(group_id, path_was, path) + repository_storage_paths = select_all("SELECT distinct(repository_storage) FROM projects WHERE namespace_id = #{group_id}").map do |row| + Gitlab.config.repositories.storages[row['repository_storage']] + end + + # Move the namespace directory in all storages paths used by member projects + repository_storage_paths.each do |repository_storage_path| + # Ensure old directory exists before moving it + gitlab_shell.add_namespace(repository_storage_path, path_was) + + unless gitlab_shell.mv_namespace(repository_storage_path, path_was, path) + Rails.logger.error "Exception moving path #{repository_storage_path} from #{path_was} to #{path}" + + # if we cannot move namespace directory we should rollback + # db changes in order to prevent out of sync between db and fs + raise Exception.new('namespace directory cannot be moved') + end + end + + Gitlab::UploadsTransfer.new.rename_namespace(path_was, path) + end +end diff --git a/db/schema.rb b/db/schema.rb index 14801b581e6..13a847827cc 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20161213172958) do +ActiveRecord::Schema.define(version: 20161220141214) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -854,7 +854,7 @@ ActiveRecord::Schema.define(version: 20161213172958) do t.datetime "expires_at" t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.string "scopes", default: "--- []\n", null: false + t.string "scopes", default: "--- []\n", null: false end add_index "personal_access_tokens", ["token"], name: "index_personal_access_tokens_on_token", unique: true, using: :btree diff --git a/doc/README.md b/doc/README.md index 8bf33cad5e4..ee69684b53b 100644 --- a/doc/README.md +++ b/doc/README.md @@ -34,7 +34,7 @@ - [Integration](integration/README.md) How to integrate with systems such as JIRA, Redmine, Twitter. - [Issue closing pattern](administration/issue_closing_pattern.md) Customize how to close an issue from commit messages. - [Koding](administration/integration/koding.md) Set up Koding to use with GitLab. -- [Online terminals](administration/integration/terminal.md) Provide terminal access to environments from within GitLab. +- [Web terminals](administration/integration/terminal.md) Provide terminal access to environments from within GitLab. - [Libravatar](customization/libravatar.md) Use Libravatar instead of Gravatar for user avatars. - [Log system](administration/logs.md) Log system. - [Environment Variables](administration/environment_variables.md) to configure GitLab. diff --git a/doc/administration/auth/README.md b/doc/administration/auth/README.md index 2fc5d0355b5..13bd501e397 100644 --- a/doc/administration/auth/README.md +++ b/doc/administration/auth/README.md @@ -6,7 +6,7 @@ providers. - [LDAP](ldap.md) Includes Active Directory, Apple Open Directory, Open LDAP, and 389 Server - [OmniAuth](../../integration/omniauth.md) Sign in via Twitter, GitHub, GitLab.com, Google, - Bitbucket, Facebook, Shibboleth, Crowd and Azure + Bitbucket, Facebook, Shibboleth, Crowd, Azure and Authentiq ID - [CAS](../../integration/cas.md) Configure GitLab to sign in using CAS - [SAML](../../integration/saml.md) Configure GitLab as a SAML 2.0 Service Provider - [Okta](okta.md) Configure GitLab to sign in using Okta diff --git a/doc/administration/auth/authentiq.md b/doc/administration/auth/authentiq.md new file mode 100644 index 00000000000..3f39539da95 --- /dev/null +++ b/doc/administration/auth/authentiq.md @@ -0,0 +1,69 @@ +# Authentiq OmniAuth Provider + +To enable the Authentiq OmniAuth provider for passwordless authentication you must register an application with Authentiq. + +Authentiq will generate a Client ID and the accompanying Client Secret for you to use. + +1. Get your Client credentials (Client ID and Client Secret) at [Authentiq](https://www.authentiq.com/register). + +2. On your GitLab server, open the configuration file: + + For omnibus installation + ```sh + sudo editor /etc/gitlab/gitlab.rb + ``` + + For installations from source: + + ```sh + sudo -u git -H editor /home/git/gitlab/config/gitlab.yml + ``` + +3. See [Initial OmniAuth Configuration](../../integration/omniauth.md#initial-omniauth-configuration) for initial settings to enable single sign-on and add Authentiq as an OAuth provider. + +4. Add the provider configuration for Authentiq: + + For Omnibus packages: + + ```ruby + gitlab_rails['omniauth_providers'] = [ + { + "name" => "authentiq", + "app_id" => "YOUR_CLIENT_ID", + "app_secret" => "YOUR_CLIENT_SECRET", + "args" => { + scope: 'aq:name email~rs aq:push' + } + } + ] + ``` + + For installations from source: + + ```yaml + - { name: 'authentiq', + app_id: 'YOUR_CLIENT_ID', + app_secret: 'YOUR_CLIENT_SECRET', + args: { + scope: 'aq:name email~rs aq:push' + } + } + ``` + + +5. The `scope` is set to request the user's name, email (required and signed), and permission to send push notifications to sign in on subsequent visits. +See [OmniAuth Authentiq strategy](https://github.com/AuthentiqID/omniauth-authentiq#scopes-and-redirect-uri-configuration) for more information on scopes and modifiers. + +6. Change 'YOUR_CLIENT_ID' and 'YOUR_CLIENT_SECRET' to the Client credentials you received in step 1. + +7. Save the configuration file. + +8. [Reconfigure](../restart_gitlab.md#omnibus-gitlab-reconfigure) or [restart GitLab](../restart_gitlab.md#installations-from-source) + for the changes to take effect if you installed GitLab via Omnibus or from source respectively. + +On the sign in page there should now be an Authentiq icon below the regular sign in form. Click the icon to begin the authentication process. + +- If the user has the Authentiq ID app installed in their iOS or Android device, they can scan the QR code, decide what personal details to share and sign in to your GitLab installation. +- If not they will be prompted to download the app and then follow the procedure above. + +If everything goes right, the user will be returned to GitLab and will be signed in.
\ No newline at end of file diff --git a/doc/administration/high_availability/load_balancer.md b/doc/administration/high_availability/load_balancer.md index e61ea359a6a..1824829903c 100644 --- a/doc/administration/high_availability/load_balancer.md +++ b/doc/administration/high_availability/load_balancer.md @@ -50,11 +50,11 @@ Read more on high-availability configuration: 1. [Configure NFS](nfs.md) 1. [Configure the GitLab application servers](gitlab.md) -[^1]: [Terminal support](../../ci/environments.md#terminal-support) requires +[^1]: [Web terminal](../../ci/environments.md#web-terminals) support requires your load balancer to correctly handle WebSocket connections. When using HTTP or HTTPS proxying, this means your load balancer must be configured to pass through the `Connection` and `Upgrade` hop-by-hop headers. See the - [online terminal](../integration/terminal.md) integration guide for + [web terminal](../integration/terminal.md) integration guide for more details. [^2]: When using HTTPS protocol for port 443, you will need to add an SSL certificate to the load balancers. If you wish to terminate SSL at the diff --git a/doc/administration/integration/terminal.md b/doc/administration/integration/terminal.md index 05d0a97e554..a1d1bb03b50 100644 --- a/doc/administration/integration/terminal.md +++ b/doc/administration/integration/terminal.md @@ -1,17 +1,17 @@ -# Online terminals +# Web terminals > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7690) -in GitLab 8.15. Only project masters and owners can access online terminals. +in GitLab 8.15. Only project masters and owners can access web terminals. With the introduction of the [Kubernetes](../../project_services/kubernetes.md) project service, GitLab gained the ability to store and use credentials for a Kubernetes cluster. One of the things it uses these credentials for is providing -access to [online terminals](../../ci/environments.html#online-terminals) +access to [web terminals](../../ci/environments.html#web-terminals) for environments. ## How it works -A detailed overview of the architecture of online terminals and how they work +A detailed overview of the architecture of web terminals and how they work can be found in [this document](https://gitlab.com/gitlab-org/gitlab-workhorse/blob/master/doc/terminal.md). In brief: @@ -31,7 +31,7 @@ In brief: ## Enabling and disabling terminal support -As online terminals use WebSockets, every HTTP/HTTPS reverse proxy in front of +As web terminals use WebSockets, every HTTP/HTTPS reverse proxy in front of Workhorse needs to be configured to pass the `Connection` and `Upgrade` headers through to the next one in the chain. If you installed Gitlab using Omnibus, or from source, starting with GitLab 8.15, this should be done by the default @@ -56,7 +56,7 @@ Omnibus installation before upgrading to 8.15, you may need to make some changes to your configuration. See the [8.14 to 8.15 upgrade](../../update/8.14-to-8.15.md#nginx-configuration) document for more details. -If you'd like to disable online terminal support in GitLab, just stop passing +If you'd like to disable web terminal support in GitLab, just stop passing the `Connection` and `Upgrade` hop-by-hop headers in the *first* HTTP reverse proxy in the chain. For most users, this will be the NGINX server bundled with Omnibus Gitlab, in which case, you need to: @@ -69,5 +69,5 @@ For your own load balancer, just reverse the configuration changes recommended by the above guides. When these headers are not passed through, Workhorse will return a -`400 Bad Request` response to users attempting to use an online terminal. In -turn, they will receive a `Connection failed` message. +`400 Bad Request` response to users attempting to use a web terminal. In turn, +they will receive a `Connection failed` message. diff --git a/doc/ci/environments.md b/doc/ci/environments.md index 07d92bb746c..98cd29c9567 100644 --- a/doc/ci/environments.md +++ b/doc/ci/environments.md @@ -27,7 +27,7 @@ so every environment can have one or more deployments. GitLab keeps track of your deployments, so you always know what is currently being deployed on your servers. If you have a deployment service such as [Kubernetes][kubernetes-service] enabled for your project, you can use it to assist with your deployments, and -can even access a terminal for your environment from within GitLab! +can even access a web terminal for your environment from within GitLab! To better understand how environments and deployments work, let's consider an example. We assume that you have already created a project in GitLab and set up @@ -235,10 +235,10 @@ Remember that if your environment's name is `production` (all lowercase), then it will get recorded in [Cycle Analytics](../user/project/cycle_analytics.md). Double the benefit! -## Terminal support +## Web terminals >**Note:** -Terminal support was added in GitLab 8.15 and is only available to project +Web terminals were added in GitLab 8.15 and are only available to project masters and owners. If you deploy to your environments with the help of a deployment service (e.g., diff --git a/doc/integration/README.md b/doc/integration/README.md index f8ffa6dcb7f..ed843c0bfa9 100644 --- a/doc/integration/README.md +++ b/doc/integration/README.md @@ -8,7 +8,7 @@ See the documentation below for details on how to configure these services. - [JIRA](../project_services/jira.md) Integrate with the JIRA issue tracker - [External issue tracker](external-issue-tracker.md) Redmine, JIRA, etc. - [LDAP](ldap.md) Set up sign in via LDAP -- [OmniAuth](omniauth.md) Sign in via Twitter, GitHub, GitLab.com, Google, Bitbucket, Facebook, Shibboleth, SAML, Crowd and Azure +- [OmniAuth](omniauth.md) Sign in via Twitter, GitHub, GitLab.com, Google, Bitbucket, Facebook, Shibboleth, SAML, Crowd, Azure and Authentiq ID - [SAML](saml.md) Configure GitLab as a SAML 2.0 Service Provider - [CAS](cas.md) Configure GitLab to sign in using CAS - [OAuth2 provider](oauth_provider.md) OAuth2 application creation diff --git a/doc/integration/omniauth.md b/doc/integration/omniauth.md index 8a55fce96fe..4c933cef9b7 100644 --- a/doc/integration/omniauth.md +++ b/doc/integration/omniauth.md @@ -30,6 +30,7 @@ contains some settings that are common for all providers. - [Crowd](crowd.md) - [Azure](azure.md) - [Auth0](auth0.md) +- [Authentiq](../administration/auth/authentiq.md) ## Initial OmniAuth Configuration diff --git a/doc/project_services/kubernetes.md b/doc/project_services/kubernetes.md index 0c5c88dd983..59d5da702f8 100644 --- a/doc/project_services/kubernetes.md +++ b/doc/project_services/kubernetes.md @@ -48,16 +48,16 @@ GitLab CI build environment: - `KUBE_NAMESPACE` - `KUBE_CA_PEM` - only if a custom CA bundle was specified -## Terminal support +## Web terminals >**NOTE:** Added in GitLab 8.15. You must be the project owner or have `master` permissions to use terminals. Support is currently limited to the first container in the first pod of your environment. -When enabled, the Kubernetes service adds online [terminal support](../ci/environments.md#terminal-support) -to your environments. This is based on the `exec` functionality found in +When enabled, the Kubernetes service adds [web terminal](../ci/environments.md#web-terminals) +support to your environments. This is based on the `exec` functionality found in Docker and Kubernetes, so you get a new shell session within your existing containers. To use this integration, you should deploy to Kubernetes using the deployment variables above, ensuring any pods you create are labelled with -`app=$CI_ENVIRONMENT_SLUG`. +`app=$CI_ENVIRONMENT_SLUG`. GitLab will do the rest! diff --git a/lib/api/services.rb b/lib/api/services.rb index aa97f6af0b2..d11cdce4e18 100644 --- a/lib/api/services.rb +++ b/lib/api/services.rb @@ -480,7 +480,7 @@ module API desc: 'The description of the tracker' } ], - 'slack-notification' => [ + 'slack' => [ { required: true, name: :webhook, @@ -500,7 +500,7 @@ module API desc: 'The channel name' } ], - 'mattermost-notification' => [ + 'mattermost' => [ { required: true, name: :webhook, diff --git a/lib/ci/api/helpers.rb b/lib/ci/api/helpers.rb index 31fbd1da108..5ff25a3a9b2 100644 --- a/lib/ci/api/helpers.rb +++ b/lib/ci/api/helpers.rb @@ -60,7 +60,7 @@ module Ci end def build_not_found! - if headers['User-Agent'].match(/gitlab-ci-multi-runner \d+\.\d+\.\d+(~beta\.\d+\.g[0-9a-f]+)? /) + if headers['User-Agent'].to_s.match(/gitlab-ci-multi-runner \d+\.\d+\.\d+(~beta\.\d+\.g[0-9a-f]+)? /) no_content! else not_found! diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb index 65b229ca8ff..7a649f28340 100644 --- a/lib/gitlab/import_export/relation_factory.rb +++ b/lib/gitlab/import_export/relation_factory.rb @@ -22,7 +22,7 @@ module Gitlab IMPORTED_OBJECT_MAX_RETRIES = 5.freeze - EXISTING_OBJECT_CHECK = %i[milestone milestones label labels project_label project_labels project_label group_label].freeze + EXISTING_OBJECT_CHECK = %i[milestone milestones label labels project_label project_labels group_label group_labels].freeze def self.create(*args) new(*args).create @@ -189,7 +189,7 @@ module Gitlab # Otherwise always create the record, skipping the extra SELECT clause. @existing_or_new_object ||= begin if EXISTING_OBJECT_CHECK.include?(@relation_name) - attribute_hash = attribute_hash_for(['events', 'priorities']) + attribute_hash = attribute_hash_for(['events']) existing_object.assign_attributes(attribute_hash) if attribute_hash.any? @@ -210,9 +210,8 @@ module Gitlab def existing_object @existing_object ||= begin - finder_attributes = @relation_name == :group_label ? %w[title group_id] : %w[title project_id] - finder_hash = parsed_relation_hash.slice(*finder_attributes) - existing_object = relation_class.find_or_create_by(finder_hash) + existing_object = find_or_create_object! + # Done in two steps, as MySQL behaves differently than PostgreSQL using # the +find_or_create_by+ method and does not return the ID the second time. existing_object.update!(parsed_relation_hash) @@ -224,6 +223,25 @@ module Gitlab @relation_name == :services && parsed_relation_hash['type'] && !Object.const_defined?(parsed_relation_hash['type']) end + + def find_or_create_object! + finder_attributes = @relation_name == :group_label ? %w[title group_id] : %w[title project_id] + finder_hash = parsed_relation_hash.slice(*finder_attributes) + + if label? + label = relation_class.find_or_initialize_by(finder_hash) + parsed_relation_hash.delete('priorities') if label.persisted? + + label.save! + label + else + relation_class.find_or_create_by(finder_hash) + end + end + + def label? + @relation_name.to_s.include?('label') + end end end end diff --git a/lib/gitlab/middleware/multipart.rb b/lib/gitlab/middleware/multipart.rb index 65713e73a59..dd99f9bb7d7 100644 --- a/lib/gitlab/middleware/multipart.rb +++ b/lib/gitlab/middleware/multipart.rb @@ -42,7 +42,7 @@ module Gitlab key, value = parsed_field.first if value.nil? - value = File.open(tmp_path) + value = open_file(tmp_path) @open_files << value else value = decorate_params_value(value, @request.params[key], tmp_path) @@ -68,7 +68,7 @@ module Gitlab case path_value when nil - value_hash[path_key] = File.open(tmp_path) + value_hash[path_key] = open_file(tmp_path) @open_files << value_hash[path_key] value_hash when Hash @@ -78,6 +78,10 @@ module Gitlab raise "unexpected path value: #{path_value.inspect}" end end + + def open_file(path) + ::UploadedFile.new(path, File.basename(path), 'application/octet-stream') + end end def initialize(app) diff --git a/lib/gitlab/update_path_error.rb b/lib/gitlab/update_path_error.rb new file mode 100644 index 00000000000..ce14cc887d0 --- /dev/null +++ b/lib/gitlab/update_path_error.rb @@ -0,0 +1,3 @@ +module Gitlab + class UpdatePathError < StandardError; end +end diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb index a763e2c5ba8..98dfb3e5216 100644 --- a/spec/controllers/groups_controller_spec.rb +++ b/spec/controllers/groups_controller_spec.rb @@ -105,4 +105,25 @@ describe GroupsController do end end end + + describe 'PUT update' do + before do + sign_in(user) + end + + it 'updates the path succesfully' do + post :update, id: group.to_param, group: { path: 'new_path' } + + expect(response).to have_http_status(302) + expect(controller).to set_flash[:notice] + end + + it 'does not update the path on error' do + allow_any_instance_of(Group).to receive(:move_dir).and_raise(Gitlab::UpdatePathError) + post :update, id: group.to_param, group: { path: 'new_path' } + + expect(assigns(:group).errors).not_to be_empty + expect(assigns(:group).path).not_to eq('new_path') + end + end end diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb new file mode 100644 index 00000000000..5fe7e6407cc --- /dev/null +++ b/spec/controllers/projects/pipelines_controller_spec.rb @@ -0,0 +1,47 @@ +require 'spec_helper' + +describe Projects::PipelinesController do + include ApiHelpers + + let(:user) { create(:user) } + let(:project) { create(:empty_project, :public) } + let(:pipeline) { create(:ci_pipeline, project: project) } + + before do + sign_in(user) + end + + describe 'GET stages.json' do + context 'when accessing existing stage' do + before do + create(:ci_build, pipeline: pipeline, stage: 'build') + + get_stage('build') + end + + it 'returns html source for stage dropdown' do + expect(response).to have_http_status(:ok) + expect(response).to render_template('projects/pipelines/_stage') + expect(json_response).to include('html') + end + end + + context 'when accessing unknown stage' do + before do + get_stage('test') + end + + it 'responds with not found' do + expect(response).to have_http_status(:not_found) + end + end + + def get_stage(name) + get :stage, namespace_id: project.namespace.path, + project_id: project.path, + id: pipeline.id, + stage: name, + format: :json + end + end +end diff --git a/spec/features/milestones/show_spec.rb b/spec/features/milestones/show_spec.rb new file mode 100644 index 00000000000..40b4dc63697 --- /dev/null +++ b/spec/features/milestones/show_spec.rb @@ -0,0 +1,26 @@ +require 'rails_helper' + +describe 'Milestone show', feature: true do + let(:user) { create(:user) } + let(:project) { create(:empty_project) } + let(:milestone) { create(:milestone, project: project) } + let(:labels) { create_list(:label, 2, project: project) } + let(:issue_params) { { project: project, assignee: user, author: user, milestone: milestone, labels: labels } } + + before do + project.add_user(user, :developer) + login_as(user) + end + + def visit_milestone + visit namespace_project_milestone_path(project.namespace, project, milestone) + end + + it 'avoids N+1 database queries' do + create(:labeled_issue, issue_params) + control_count = ActiveRecord::QueryRecorder.new { visit_milestone }.count + create_list(:labeled_issue, 10, issue_params) + + expect { visit_milestone }.not_to exceed_query_limit(control_count) + end +end diff --git a/spec/features/projects/import_export/test_project_export.tar.gz b/spec/features/projects/import_export/test_project_export.tar.gz Binary files differindex d3165d07d7b..7655c2b351f 100644 --- a/spec/features/projects/import_export/test_project_export.tar.gz +++ b/spec/features/projects/import_export/test_project_export.tar.gz diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb index 1210e2745db..14e009daba8 100644 --- a/spec/features/projects/pipelines/pipeline_spec.rb +++ b/spec/features/projects/pipelines/pipeline_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe "Pipelines", feature: true, js: true do +describe 'Pipeline', :feature, :js do include GitlabRoutingHelper let(:project) { create(:empty_project) } diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb index f3731698a18..1ff57f92c4c 100644 --- a/spec/features/projects/pipelines/pipelines_spec.rb +++ b/spec/features/projects/pipelines/pipelines_spec.rb @@ -1,7 +1,8 @@ require 'spec_helper' -describe "Pipelines" do +describe 'Pipelines', :feature, :js do include GitlabRoutingHelper + include WaitForAjax let(:project) { create(:empty_project) } let(:user) { create(:user) } @@ -69,16 +70,32 @@ describe "Pipelines" do end context 'with manual actions' do - let!(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'manual build', stage: 'test', commands: 'test') } + let!(:manual) do + create(:ci_build, :manual, pipeline: pipeline, + name: 'manual build', + stage: 'test', + commands: 'test') + end - before { visit namespace_project_pipelines_path(project.namespace, project) } + before do + visit namespace_project_pipelines_path(project.namespace, project) + end - it { expect(page).to have_link('Manual build') } + it 'has link to the manual action' do + find('.js-pipeline-dropdown-manual-actions').click - context 'when playing' do - before { click_link('Manual build') } + expect(page).to have_link('Manual build') + end - it { expect(manual.reload).to be_pending } + context 'when manual action was played' do + before do + find('.js-pipeline-dropdown-manual-actions').click + click_link('Manual build') + end + + it 'enqueues manual action job' do + expect(manual.reload).to be_pending + end end end @@ -131,7 +148,10 @@ describe "Pipelines" do before { visit namespace_project_pipelines_path(project.namespace, project) } it { expect(page).to have_selector('.build-artifacts') } - it { expect(page).to have_link(with_artifacts.name) } + it do + find('.js-pipeline-dropdown-download').click + expect(page).to have_link(with_artifacts.name) + end end context 'with artifacts expired' do @@ -150,6 +170,42 @@ describe "Pipelines" do it { expect(page).not_to have_selector('.build-artifacts') } end end + + context 'mini pipleine graph' do + let!(:build) do + create(:ci_build, pipeline: pipeline, stage: 'build', name: 'build') + end + + before do + visit namespace_project_pipelines_path(project.namespace, project) + end + + it 'should render a mini pipeline graph' do + endpoint = stage_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline, stage: build.name) + + expect(page).to have_selector('.mini-pipeline-graph') + expect(page).to have_selector(".js-builds-dropdown-button[data-stage-endpoint='#{endpoint}']") + end + + context 'when clicking a graph stage' do + it 'should open a dropdown' do + find('.js-builds-dropdown-button').trigger('click') + + wait_for_ajax + + expect(page).to have_link build.name + end + + it 'should be possible to retry the failed build' do + find('.js-builds-dropdown-button').trigger('click') + + wait_for_ajax + + find('a.ci-action-icon-container').trigger('click') + expect(page).not_to have_content('Cancel running') + end + end + end end describe 'POST /:project/pipelines' do diff --git a/spec/features/projects/services/slack_service_spec.rb b/spec/features/projects/services/slack_service_spec.rb index 320ed13a01d..16541f51d98 100644 --- a/spec/features/projects/services/slack_service_spec.rb +++ b/spec/features/projects/services/slack_service_spec.rb @@ -2,8 +2,8 @@ require 'spec_helper' feature 'Projects > Slack service > Setup events', feature: true do let(:user) { create(:user) } - let(:service) { SlackNotificationService.new } - let(:project) { create(:project, slack_notification_service: service) } + let(:service) { SlackService.new } + let(:project) { create(:project, slack_service: service) } background do service.fields diff --git a/spec/javascripts/fixtures/issuable_filter.html.haml b/spec/javascripts/fixtures/issuable_filter.html.haml new file mode 100644 index 00000000000..ae745b292e6 --- /dev/null +++ b/spec/javascripts/fixtures/issuable_filter.html.haml @@ -0,0 +1,8 @@ +%form.js-filter-form{action: '/user/project/issues?scope=all&state=closed'} + %input{id: 'utf8', name: 'utf8', value: '✓'} + %input{id: 'check_all_issues', name: 'check_all_issues'} + %input{id: 'search', name: 'search'} + %input{id: 'author_id', name: 'author_id'} + %input{id: 'assignee_id', name: 'assignee_id'} + %input{id: 'milestone_title', name: 'milestone_title'} + %input{id: 'label_name', name: 'label_name'} diff --git a/spec/javascripts/fixtures/mini_dropdown_graph.html.haml b/spec/javascripts/fixtures/mini_dropdown_graph.html.haml new file mode 100644 index 00000000000..e9bf7568e95 --- /dev/null +++ b/spec/javascripts/fixtures/mini_dropdown_graph.html.haml @@ -0,0 +1,8 @@ +%div.js-builds-dropdown-tests + %button.dropdown.js-builds-dropdown-button{'data-stage-endpoint' => 'foobar'} + Dropdown + %div.js-builds-dropdown-container + %div.js-builds-dropdown-list + + %div.js-builds-dropdown-loading.builds-dropdown-loading.hidden + %span.fa.fa-spinner.fa-spin diff --git a/spec/javascripts/issuable_spec.js.es6 b/spec/javascripts/issuable_spec.js.es6 new file mode 100644 index 00000000000..d61601ee4fb --- /dev/null +++ b/spec/javascripts/issuable_spec.js.es6 @@ -0,0 +1,81 @@ +/* global Issuable */ +/* global Turbolinks */ + +//= require issuable +//= require turbolinks + +(() => { + const BASE_URL = '/user/project/issues?scope=all&state=closed'; + const DEFAULT_PARAMS = '&utf8=%E2%9C%93'; + + function updateForm(formValues, form) { + $.each(formValues, (id, value) => { + $(`#${id}`, form).val(value); + }); + } + + function resetForm(form) { + $('input[name!="utf8"]', form).each((index, input) => { + input.setAttribute('value', ''); + }); + } + + describe('Issuable', () => { + fixture.preload('issuable_filter'); + + beforeEach(() => { + fixture.load('issuable_filter'); + Issuable.init(); + }); + + it('should be defined', () => { + expect(window.Issuable).toBeDefined(); + }); + + describe('filtering', () => { + let $filtersForm; + + beforeEach(() => { + $filtersForm = $('.js-filter-form'); + fixture.load('issuable_filter'); + resetForm($filtersForm); + }); + + it('should contain only the default parameters', () => { + spyOn(Turbolinks, 'visit'); + + Issuable.filterResults($filtersForm); + + expect(Turbolinks.visit).toHaveBeenCalledWith(BASE_URL + DEFAULT_PARAMS); + }); + + it('should filter for the phrase "broken"', () => { + spyOn(Turbolinks, 'visit'); + + updateForm({ search: 'broken' }, $filtersForm); + Issuable.filterResults($filtersForm); + const params = `${DEFAULT_PARAMS}&search=broken`; + + expect(Turbolinks.visit).toHaveBeenCalledWith(BASE_URL + params); + }); + + it('should keep query parameters after modifying filter', () => { + spyOn(Turbolinks, 'visit'); + + // initial filter + updateForm({ milestone_title: 'v1.0' }, $filtersForm); + + Issuable.filterResults($filtersForm); + let params = `${DEFAULT_PARAMS}&milestone_title=v1.0`; + expect(Turbolinks.visit).toHaveBeenCalledWith(BASE_URL + params); + + // update filter + updateForm({ label_name: 'Frontend' }, $filtersForm); + + Issuable.filterResults($filtersForm); + params = `${DEFAULT_PARAMS}&milestone_title=v1.0&label_name=Frontend`; + expect(Turbolinks.visit).toHaveBeenCalledWith(BASE_URL + params); + }); + }); + }); +})(); diff --git a/spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6 b/spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6 new file mode 100644 index 00000000000..d1793e9308e --- /dev/null +++ b/spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6 @@ -0,0 +1,51 @@ +/* eslint-disable no-new */ + +//= require flash +//= require mini_pipeline_graph_dropdown + +(() => { + describe('Mini Pipeline Graph Dropdown', () => { + fixture.preload('mini_dropdown_graph'); + + beforeEach(() => { + fixture.load('mini_dropdown_graph'); + }); + + describe('When is initialized', () => { + it('should initialize without errors when no options are given', () => { + const miniPipelineGraph = new window.gl.MiniPipelineGraph(); + + expect(miniPipelineGraph.dropdownListSelector).toEqual('.js-builds-dropdown-container'); + }); + + it('should set the container as the given prop', () => { + const container = '.foo'; + + const miniPipelineGraph = new window.gl.MiniPipelineGraph({ container }); + + expect(miniPipelineGraph.container).toEqual(container); + }); + }); + + describe('When dropdown is clicked', () => { + it('should call getBuildsList', () => { + const getBuildsListSpy = spyOn(gl.MiniPipelineGraph.prototype, 'getBuildsList').and.callFake(function () {}); + + new gl.MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }); + + document.querySelector('.js-builds-dropdown-button').click(); + + expect(getBuildsListSpy).toHaveBeenCalled(); + }); + + it('should make a request to the endpoint provided in the html', () => { + const ajaxSpy = spyOn($, 'ajax').and.callFake(function () {}); + + new gl.MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }); + + document.querySelector('.js-builds-dropdown-button').click(); + expect(ajaxSpy.calls.allArgs()[0][0].url).toEqual('foobar'); + }); + }); + }); +})(); diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 7e618e2fcf5..f420d71dee2 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -137,8 +137,8 @@ project: - assembla_service - asana_service - gemnasium_service -- slack_notification_service -- mattermost_notification_service +- slack_service +- mattermost_service - buildkite_service - bamboo_service - teamcity_service diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json index 931d426c87f..2c0750c3377 100644 --- a/spec/lib/gitlab/import_export/project.json +++ b/spec/lib/gitlab/import_export/project.json @@ -15,6 +15,28 @@ "type": "ProjectLabel", "priorities": [ ] + }, + { + "id": 3, + "title": "test3", + "color": "#428bca", + "group_id": 8, + "created_at": "2016-07-22T08:55:44.161Z", + "updated_at": "2016-07-22T08:55:44.161Z", + "template": false, + "description": "", + "project_id": null, + "type": "GroupLabel", + "priorities": [ + { + "id": 1, + "project_id": 5, + "label_id": 1, + "priority": 1, + "created_at": "2016-10-18T09:35:43.338Z", + "updated_at": "2016-10-18T09:35:43.338Z" + } + ] } ], "issues": [ diff --git a/spec/lib/gitlab/middleware/multipart_spec.rb b/spec/lib/gitlab/middleware/multipart_spec.rb index ab1ab22795c..8d925460f01 100644 --- a/spec/lib/gitlab/middleware/multipart_spec.rb +++ b/spec/lib/gitlab/middleware/multipart_spec.rb @@ -12,7 +12,7 @@ describe Gitlab::Middleware::Multipart do expect(app).to receive(:call) do |env| file = Rack::Request.new(env).params['file'] - expect(file).to be_a(File) + expect(file).to be_a(::UploadedFile) expect(file.path).to eq(tempfile.path) end @@ -39,7 +39,7 @@ describe Gitlab::Middleware::Multipart do expect(app).to receive(:call) do |env| file = Rack::Request.new(env).params['user']['avatar'] - expect(file).to be_a(File) + expect(file).to be_a(::UploadedFile) expect(file.path).to eq(tempfile.path) end @@ -54,7 +54,7 @@ describe Gitlab::Middleware::Multipart do expect(app).to receive(:call) do |env| file = Rack::Request.new(env).params['project']['milestone']['themesong'] - expect(file).to be_a(File) + expect(file).to be_a(::UploadedFile) expect(file.path).to eq(tempfile.path) end diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 52dd41065e9..dc377d15f15 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -175,6 +175,30 @@ describe Ci::Pipeline, models: true do end end + describe '#stage' do + subject { pipeline.stage('test') } + + context 'with status in stage' do + before do + create(:commit_status, pipeline: pipeline, stage: 'test') + end + + it { expect(subject).to be_a Ci::Stage } + it { expect(subject.name).to eq 'test' } + it { expect(subject.statuses).not_to be_empty } + end + + context 'without status in stage' do + before do + create(:commit_status, pipeline: pipeline, stage: 'build') + end + + it 'return stage object' do + is_expected.to be_nil + end + end + end + describe 'state machine' do let(:current) { Time.now.change(usec: 0) } let(:build) { create_build('build1', 0) } diff --git a/spec/models/ci/stage_spec.rb b/spec/models/ci/stage_spec.rb index 8fff38f7cda..742bedb37e4 100644 --- a/spec/models/ci/stage_spec.rb +++ b/spec/models/ci/stage_spec.rb @@ -28,6 +28,19 @@ describe Ci::Stage, models: true do end end + describe '#statuses_count' do + before do + create_job(:ci_build) + create_job(:ci_build, stage: 'other stage') + end + + subject { stage.statuses_count } + + it "counts statuses only from current stage" do + is_expected.to eq(1) + end + end + describe '#builds' do let!(:stage_build) { create_job(:ci_build) } let!(:commit_status) { create_job(:commit_status) } diff --git a/spec/models/project_services/slack_notification_service_spec.rb b/spec/models/project_services/mattermost_service_spec.rb index 110b5bf2115..490d6aedffc 100644 --- a/spec/models/project_services/slack_notification_service_spec.rb +++ b/spec/models/project_services/mattermost_service_spec.rb @@ -1,5 +1,5 @@ require 'spec_helper' -describe SlackNotificationService, models: true do +describe MattermostService, models: true do it_behaves_like "slack or mattermost notifications" end diff --git a/spec/models/project_services/mattermost_notification_service_spec.rb b/spec/models/project_services/slack_service_spec.rb index 7832d6f50cf..9a3ecc66d83 100644 --- a/spec/models/project_services/mattermost_notification_service_spec.rb +++ b/spec/models/project_services/slack_service_spec.rb @@ -1,5 +1,5 @@ require 'spec_helper' -describe MattermostNotificationService, models: true do +describe SlackService, models: true do it_behaves_like "slack or mattermost notifications" end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 0455cd2fe49..88d5d14f855 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -21,8 +21,8 @@ describe Project, models: true do it { is_expected.to have_many(:hooks).dependent(:destroy) } it { is_expected.to have_many(:protected_branches).dependent(:destroy) } it { is_expected.to have_one(:forked_project_link).dependent(:destroy) } - it { is_expected.to have_one(:slack_notification_service).dependent(:destroy) } - it { is_expected.to have_one(:mattermost_notification_service).dependent(:destroy) } + it { is_expected.to have_one(:slack_service).dependent(:destroy) } + it { is_expected.to have_one(:mattermost_service).dependent(:destroy) } it { is_expected.to have_one(:pushover_service).dependent(:destroy) } it { is_expected.to have_one(:asana_service).dependent(:destroy) } it { is_expected.to have_many(:boards).dependent(:destroy) } diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb index 79f12ace999..3b5dc98e4d5 100644 --- a/spec/requests/ci/api/builds_spec.rb +++ b/spec/requests/ci/api/builds_spec.rb @@ -37,6 +37,11 @@ describe Ci::API::Builds do let(:user_agent) { 'Go-http-client/1.1' } it { expect(response).to have_http_status(404) } end + + context "when runner doesn't have a User-Agent" do + let(:user_agent) { nil } + it { expect(response).to have_http_status(404) } + end end context 'when there is a pending build' do diff --git a/spec/services/groups/update_service_spec.rb b/spec/services/groups/update_service_spec.rb index 9c2331144a0..531180e48a1 100644 --- a/spec/services/groups/update_service_spec.rb +++ b/spec/services/groups/update_service_spec.rb @@ -1,15 +1,15 @@ require 'spec_helper' describe Groups::UpdateService, services: true do - let!(:user) { create(:user) } - let!(:private_group) { create(:group, :private) } - let!(:internal_group) { create(:group, :internal) } - let!(:public_group) { create(:group, :public) } + let!(:user) { create(:user) } + let!(:private_group) { create(:group, :private) } + let!(:internal_group) { create(:group, :internal) } + let!(:public_group) { create(:group, :public) } describe "#execute" do context "project visibility_level validation" do context "public group with public projects" do - let!(:service) { described_class.new(public_group, user, visibility_level: Gitlab::VisibilityLevel::INTERNAL ) } + let!(:service) { described_class.new(public_group, user, visibility_level: Gitlab::VisibilityLevel::INTERNAL) } before do public_group.add_user(user, Gitlab::Access::MASTER) @@ -23,7 +23,7 @@ describe Groups::UpdateService, services: true do end context "internal group with internal project" do - let!(:service) { described_class.new(internal_group, user, visibility_level: Gitlab::VisibilityLevel::PRIVATE ) } + let!(:service) { described_class.new(internal_group, user, visibility_level: Gitlab::VisibilityLevel::PRIVATE) } before do internal_group.add_user(user, Gitlab::Access::MASTER) @@ -39,7 +39,7 @@ describe Groups::UpdateService, services: true do end context "unauthorized visibility_level validation" do - let!(:service) { described_class.new(internal_group, user, visibility_level: 99 ) } + let!(:service) { described_class.new(internal_group, user, visibility_level: 99) } before do internal_group.add_user(user, Gitlab::Access::MASTER) end @@ -49,4 +49,41 @@ describe Groups::UpdateService, services: true do expect(internal_group.errors.count).to eq(1) end end + + context 'rename group' do + let!(:service) { described_class.new(internal_group, user, path: 'new_path') } + + before do + internal_group.add_user(user, Gitlab::Access::MASTER) + create(:project, :internal, group: internal_group) + end + + it 'returns true' do + expect(service.execute).to eq(true) + end + + context 'error moving group' do + before do + allow(internal_group).to receive(:move_dir).and_raise(Gitlab::UpdatePathError) + end + + it 'does not raise an error' do + expect { service.execute }.not_to raise_error + end + + it 'returns false' do + expect(service.execute).to eq(false) + end + + it 'has the right error' do + service.execute + + expect(internal_group.errors.full_messages.first).to eq('Gitlab::UpdatePathError') + end + + it "hasn't changed the path" do + expect { service.execute}.not_to change { internal_group.reload.path} + end + end + end end diff --git a/spec/support/query_recorder.rb b/spec/support/query_recorder.rb new file mode 100644 index 00000000000..e40d5ebd9a8 --- /dev/null +++ b/spec/support/query_recorder.rb @@ -0,0 +1,40 @@ +module ActiveRecord + class QueryRecorder + attr_reader :log + + def initialize(&block) + @log = [] + ActiveSupport::Notifications.subscribed(method(:callback), 'sql.active_record', &block) + end + + def callback(name, start, finish, message_id, values) + return if %w(CACHE SCHEMA).include?(values[:name]) + @log << values[:sql] + end + + def count + @log.count + end + + def log_message + @log.join("\n\n") + end + end +end + +RSpec::Matchers.define :exceed_query_limit do |expected| + supports_block_expectations + + match do |block| + query_count(&block) > expected + end + + failure_message_when_negated do |actual| + "Expected a maximum of #{expected} queries, got #{@recorder.count}:\n\n#{@recorder.log_message}" + end + + def query_count(&block) + @recorder = ActiveRecord::QueryRecorder.new(&block) + @recorder.count + end +end diff --git a/spec/views/projects/pipelines/_stage.html.haml_spec.rb b/spec/views/projects/pipelines/_stage.html.haml_spec.rb new file mode 100644 index 00000000000..eb7f7ca4a1a --- /dev/null +++ b/spec/views/projects/pipelines/_stage.html.haml_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe 'projects/pipelines/_stage', :view do + let(:project) { create(:project) } + let(:pipeline) { create(:ci_pipeline, project: project) } + let(:stage) { build(:ci_stage, pipeline: pipeline) } + + before do + assign :stage, stage + + create(:ci_build, name: 'test:build', + stage: stage.name, + pipeline: pipeline) + end + + it 'shows the builds in the stage' do + render + + expect(rendered).to have_text 'test:build' + end +end |