diff options
192 files changed, 4252 insertions, 974 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 5723b836c76..a3ce1de50c2 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -474,8 +474,6 @@ codeclimate: services: - docker:dind script: - - docker pull stedolan/jq - - docker pull codeclimate/codeclimate - docker run --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock --volume /tmp/cc:/tmp/cc codeclimate/codeclimate analyze -f json > raw_codeclimate.json - cat raw_codeclimate.json | docker run -i stedolan/jq -c 'map({check_name,fingerprint,location})' > codeclimate.json artifacts: diff --git a/CHANGELOG.md b/CHANGELOG.md index 7591559da22..ec768ffe9b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 9.3.4 (2017-07-03) + +- No changes. + ## 9.3.3 (2017-06-30) - Fix head pipeline stored in merge request for external pipelines. !12478 diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION index c20c645d7e4..ac14c3dfaa8 100644 --- a/GITLAB_SHELL_VERSION +++ b/GITLAB_SHELL_VERSION @@ -1 +1 @@ -5.0.6 +5.1.1 @@ -255,7 +255,7 @@ gem 'net-ssh', '~> 3.0.1' gem 'base32', '~> 0.3.0' # Sentry integration -gem 'sentry-raven', '~> 2.4.0' +gem 'sentry-raven', '~> 2.5.3' gem 'premailer-rails', '~> 1.9.7' @@ -285,6 +285,7 @@ group :metrics do # Prometheus gem 'prometheus-client-mmap', '~>0.7.0.beta5' + gem 'raindrops', '~> 0.18' end group :development do diff --git a/Gemfile.lock b/Gemfile.lock index f4ddd30da1b..70abc0669df 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -599,8 +599,8 @@ GEM premailer-rails (1.9.7) actionmailer (>= 3, < 6) premailer (~> 1.7, >= 1.7.9) - prometheus-client-mmap (0.7.0.beta5) - mmap2 (~> 2.2.6) + prometheus-client-mmap (0.7.0.beta8) + mmap2 (~> 2.2, >= 2.2.7) pry (0.10.4) coderay (~> 1.1.0) method_source (~> 0.8.1) @@ -658,7 +658,7 @@ GEM thor (>= 0.18.1, < 2.0) rainbow (2.2.2) rake - raindrops (0.17.0) + raindrops (0.18.0) rake (10.5.0) rblineprof (0.3.6) debugger-ruby_core_source (~> 1.3) @@ -775,7 +775,7 @@ GEM activesupport (>= 3.1) select2-rails (3.5.9.3) thor (~> 0.14) - sentry-raven (2.4.0) + sentry-raven (2.5.3) faraday (>= 0.7.6, < 1.0) settingslogic (2.0.9) sexp_processor (4.9.0) @@ -1062,6 +1062,7 @@ DEPENDENCIES rails-deprecated_sanitizer (~> 1.0.3) rails-i18n (~> 4.0.9) rainbow (~> 2.2) + raindrops (~> 0.18) rblineprof (~> 0.3.6) rdoc (~> 4.2) recaptcha (~> 3.0) @@ -1089,7 +1090,7 @@ DEPENDENCIES scss_lint (~> 0.47.0) seed-fu (~> 2.3.5) select2-rails (~> 3.5.9) - sentry-raven (~> 2.4.0) + sentry-raven (~> 2.5.3) settingslogic (~> 2.0.9) sham_rack (~> 1.3.6) shoulda-matchers (~> 2.8.0) diff --git a/app/assets/javascripts/behaviors/autosize.js b/app/assets/javascripts/behaviors/autosize.js index 3bea460dcc6..e00af4b2fa8 100644 --- a/app/assets/javascripts/behaviors/autosize.js +++ b/app/assets/javascripts/behaviors/autosize.js @@ -1,23 +1,8 @@ import autosize from 'vendor/autosize'; -$(() => { - const $fields = $('.js-autosize'); +document.addEventListener('DOMContentLoaded', () => { + const autosizeEls = document.querySelectorAll('.js-autosize'); - $fields.on('autosize:resized', function resized() { - const $field = $(this); - $field.data('height', $field.outerHeight()); - }); - - $fields.on('resize.autosize', function resize() { - const $field = $(this); - if ($field.data('height') !== $field.outerHeight()) { - $field.data('height', $field.outerHeight()); - autosize.destroy($field); - $field.css('max-height', window.outerHeight); - } - }); - - autosize($fields); - autosize.update($fields); - $fields.css('resize', 'vertical'); + autosize(autosizeEls); + autosize.update(autosizeEls); }); diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js index c7afd4ead6b..590b7be36e3 100644 --- a/app/assets/javascripts/boards/components/board_sidebar.js +++ b/app/assets/javascripts/boards/components/board_sidebar.js @@ -34,7 +34,10 @@ gl.issueBoards.BoardSidebar = Vue.extend({ }, milestoneTitle() { return this.issue.milestone ? this.issue.milestone.title : 'No Milestone'; - } + }, + canRemove() { + return !this.list.preset; + }, }, watch: { detail: { diff --git a/app/assets/javascripts/boards/components/sidebar/remove_issue.js b/app/assets/javascripts/boards/components/sidebar/remove_issue.js index 5597f128b80..6a900d4abd0 100644 --- a/app/assets/javascripts/boards/components/sidebar/remove_issue.js +++ b/app/assets/javascripts/boards/components/sidebar/remove_issue.js @@ -46,8 +46,7 @@ gl.issueBoards.RemoveIssueBtn = Vue.extend({ }, template: ` <div - class="block list" - v-if="list.type !== 'closed'"> + class="block list"> <button class="btn btn-default btn-block" type="button" diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index 60103155ce0..1dfa064acfd 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -13,25 +13,21 @@ window.Build = (function () { this.options = options || $('.js-build-options').data(); this.pageUrl = this.options.pageUrl; - this.buildUrl = this.options.buildUrl; this.buildStatus = this.options.buildStatus; this.state = this.options.logState; this.buildStage = this.options.buildStage; this.$document = $(document); this.logBytes = 0; - this.scrollOffsetPadding = 30; this.hasBeenScrolled = false; this.updateDropdown = this.updateDropdown.bind(this); this.getBuildTrace = this.getBuildTrace.bind(this); - this.scrollToBottom = this.scrollToBottom.bind(this); - this.$body = $('body'); this.$buildTrace = $('#build-trace'); this.$buildRefreshAnimation = $('.js-build-refresh'); this.$truncatedInfo = $('.js-truncated-info'); this.$buildTraceOutput = $('.js-build-output'); - this.$scrollContainer = $('.js-scroll-container'); + this.$topBar = $('.js-top-bar'); // Scroll controllers this.$scrollTopBtn = $('.js-scroll-up'); @@ -63,13 +59,22 @@ window.Build = (function () { .off('click') .on('click', this.scrollToBottom.bind(this)); - const scrollThrottled = _.throttle(this.toggleScroll.bind(this), 100); + this.scrollThrottled = _.throttle(this.toggleScroll.bind(this), 100); - this.$scrollContainer + $(window) .off('scroll') .on('scroll', () => { - this.hasBeenScrolled = true; - scrollThrottled(); + const contentHeight = this.$buildTraceOutput.prop('scrollHeight'); + if (contentHeight > this.windowSize) { + // means the user did not scroll, the content was updated. + this.windowSize = contentHeight; + } else { + // User scrolled + this.hasBeenScrolled = true; + this.toggleScrollAnimation(false); + } + + this.scrollThrottled(); }); $(window) @@ -77,59 +82,73 @@ window.Build = (function () { .on('resize.build', _.throttle(this.sidebarOnResize.bind(this), 100)); this.updateArtifactRemoveDate(); + this.initAffixTopArea(); - // eslint-disable-next-line - this.getBuildTrace() - .then(() => this.toggleScroll()) - .then(() => { - if (!this.hasBeenScrolled) { - this.scrollToBottom(); - } - }) - .then(() => this.verifyTopPosition()); + this.getBuildTrace(); } + Build.prototype.initAffixTopArea = function () { + /** + If the browser does not support position sticky, it returns the position as static. + If the browser does support sticky, then we allow the browser to handle it, if not + then we default back to Bootstraps affix + **/ + if (this.$topBar.css('position') !== 'static') return; + + const offsetTop = this.$buildTrace.offset().top; + + this.$topBar.affix({ + offset: { + top: offsetTop, + }, + }); + }; + Build.prototype.canScroll = function () { - return (this.$scrollContainer.prop('scrollHeight') - this.scrollOffsetPadding) > this.$scrollContainer.height(); + return document.body.scrollHeight > window.innerHeight; }; - /** - * | | Up | Down | - * |--------------------------|----------|----------| - * | on scroll bottom | active | disabled | - * | on scroll top | disabled | active | - * | no scroll | disabled | disabled | - * | on.('scroll') is on top | disabled | active | - * | on('scroll) is on bottom | active | disabled | - * - */ Build.prototype.toggleScroll = function () { - const currentPosition = this.$scrollContainer.scrollTop(); - const bottomScroll = currentPosition + this.$scrollContainer.innerHeight(); + const currentPosition = document.body.scrollTop; + const windowHeight = window.innerHeight; if (this.canScroll()) { - if (currentPosition === 0) { + if (currentPosition > 0 && + (document.body.scrollHeight - currentPosition !== windowHeight)) { + // User is in the middle of the log + + this.toggleDisableButton(this.$scrollTopBtn, false); + this.toggleDisableButton(this.$scrollBottomBtn, false); + } else if (currentPosition === 0) { + // User is at Top of Build Log + this.toggleDisableButton(this.$scrollTopBtn, true); this.toggleDisableButton(this.$scrollBottomBtn, false); - } else if (bottomScroll === this.$scrollContainer.prop('scrollHeight')) { + } else if (document.body.scrollHeight - currentPosition === windowHeight) { + // User is at the bottom of the build log. + this.toggleDisableButton(this.$scrollTopBtn, false); this.toggleDisableButton(this.$scrollBottomBtn, true); - } else { - this.toggleDisableButton(this.$scrollTopBtn, false); - this.toggleDisableButton(this.$scrollBottomBtn, false); } + } else { + this.toggleDisableButton(this.$scrollTopBtn, true); + this.toggleDisableButton(this.$scrollBottomBtn, true); } }; - Build.prototype.scrollToTop = function () { + Build.prototype.scrollDown = function () { + document.body.scrollTop = document.body.scrollHeight; + }; + + Build.prototype.scrollToBottom = function () { + this.scrollDown(); this.hasBeenScrolled = true; - this.$scrollContainer.scrollTop(0); this.toggleScroll(); }; - Build.prototype.scrollToBottom = function () { + Build.prototype.scrollToTop = function () { + document.body.scrollTop = 0; this.hasBeenScrolled = true; - this.$scrollContainer.scrollTop(this.$scrollContainer.prop('scrollHeight')); this.toggleScroll(); }; @@ -142,47 +161,6 @@ window.Build = (function () { this.$scrollBottomBtn.toggleClass('animate', toggle); }; - /** - * Build trace top position depends on the space ocupied by the elments rendered before - */ - Build.prototype.verifyTopPosition = function () { - const $buildPage = $('.build-page'); - - const $flashError = $('.alert-wrapper'); - const $header = $('.build-header', $buildPage); - const $runnersStuck = $('.js-build-stuck', $buildPage); - const $startsEnvironment = $('.js-environment-container', $buildPage); - const $erased = $('.js-build-erased', $buildPage); - const prependTopDefault = 20; - - // header + navigation + margin - let topPostion = 168; - - if ($header.length) { - topPostion += $header.outerHeight(); - } - - if ($runnersStuck.length) { - topPostion += $runnersStuck.outerHeight(); - } - - if ($startsEnvironment.length) { - topPostion += $startsEnvironment.outerHeight() + prependTopDefault; - } - - if ($erased.length) { - topPostion += $erased.outerHeight() + prependTopDefault; - } - - if ($flashError.length) { - topPostion += $flashError.outerHeight() + prependTopDefault; - } - - this.$buildTrace.css({ - top: topPostion, - }); - }; - Build.prototype.initSidebar = function () { this.$sidebar = $('.js-build-sidebar'); this.$sidebar.niceScroll(); @@ -200,6 +178,8 @@ window.Build = (function () { this.state = log.state; } + this.windowSize = this.$buildTraceOutput.prop('scrollHeight'); + if (log.append) { this.$buildTraceOutput.append(log.html); this.logBytes += log.size; @@ -227,14 +207,7 @@ window.Build = (function () { } Build.timeout = setTimeout(() => { - //eslint-disable-next-line - this.getBuildTrace() - .then(() => { - if (!this.hasBeenScrolled) { - this.scrollToBottom(); - } - }) - .then(() => this.verifyTopPosition()); + this.getBuildTrace(); }, 4000); } else { this.$buildRefreshAnimation.remove(); @@ -247,7 +220,13 @@ window.Build = (function () { }) .fail(() => { this.$buildRefreshAnimation.remove(); - }); + }) + .then(() => { + if (!this.hasBeenScrolled) { + this.scrollDown(); + } + }) + .then(() => this.toggleScroll()); }; Build.prototype.shouldHideSidebarForViewport = function () { @@ -259,14 +238,11 @@ window.Build = (function () { const shouldShow = typeof shouldHide === 'boolean' ? !shouldHide : undefined; const $toggleButton = $('.js-sidebar-build-toggle-header'); - this.$buildTrace - .toggleClass('sidebar-expanded', shouldShow) - .toggleClass('sidebar-collapsed', shouldHide); this.$sidebar .toggleClass('right-sidebar-expanded', shouldShow) .toggleClass('right-sidebar-collapsed', shouldHide); - $('.js-build-page') + this.$topBar .toggleClass('sidebar-expanded', shouldShow) .toggleClass('sidebar-collapsed', shouldHide); @@ -279,17 +255,10 @@ window.Build = (function () { Build.prototype.sidebarOnResize = function () { this.toggleSidebar(this.shouldHideSidebarForViewport()); - - this.verifyTopPosition(); - - if (this.canScroll()) { - this.toggleScroll(); - } }; Build.prototype.sidebarOnClick = function () { if (this.shouldHideSidebarForViewport()) this.toggleSidebar(); - this.verifyTopPosition(); }; Build.prototype.updateArtifactRemoveDate = function () { diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 4247540de22..e924fde60bf 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -56,6 +56,7 @@ import GfmAutoComplete from './gfm_auto_complete'; import ShortcutsBlob from './shortcuts_blob'; import initSettingsPanels from './settings_panels'; import initExperimentalFlags from './experimental_flags'; +import OAuthRememberMe from './oauth_remember_me'; (function() { var Dispatcher; @@ -127,6 +128,7 @@ import initExperimentalFlags from './experimental_flags'; case 'sessions:new': new UsernameValidator(); new ActiveTabMemoizer(); + new OAuthRememberMe({ container: $(".omniauth-container") }).bindEvents(); break; case 'projects:boards:show': case 'projects:boards:index': diff --git a/app/assets/javascripts/jobs/job_details_bundle.js b/app/assets/javascripts/jobs/job_details_bundle.js index 939d17129de..f92e669414a 100644 --- a/app/assets/javascripts/jobs/job_details_bundle.js +++ b/app/assets/javascripts/jobs/job_details_bundle.js @@ -26,14 +26,6 @@ document.addEventListener('DOMContentLoaded', () => { mounted() { this.mediator.initBuildClass(); }, - updated() { - // Wait for flash message to be appended - Vue.nextTick(() => { - if (this.mediator.build) { - this.mediator.build.verifyTopPosition(); - } - }); - }, render(createElement) { return createElement('job-header', { props: { diff --git a/app/assets/javascripts/monitoring/components/monitoring_column.vue b/app/assets/javascripts/monitoring/components/monitoring_column.vue index 4f4792877ee..0f33581ec52 100644 --- a/app/assets/javascripts/monitoring/components/monitoring_column.vue +++ b/app/assets/javascripts/monitoring/components/monitoring_column.vue @@ -35,7 +35,7 @@ data() { return { - graphHeight: 500, + graphHeight: 450, graphWidth: 600, graphHeightOffset: 120, xScale: {}, @@ -88,7 +88,9 @@ }, paddingBottomRootSvg() { - return (Math.ceil(this.graphHeight * 100) / this.graphWidth) || 0; + return { + paddingBottom: `${(Math.ceil(this.graphHeight * 100) / this.graphWidth) || 0}%`, + }; }, }, @@ -104,7 +106,7 @@ } this.data = query.result[0].values; this.unitOfDisplay = query.unit || 'N/A'; - this.yAxisLabel = this.columnData.y_axis || 'Values'; + this.yAxisLabel = this.columnData.y_label || 'Values'; this.legendTitle = query.legend || 'Average'; this.graphWidth = this.$refs.baseSvg.clientWidth - this.margin.left - this.margin.right; @@ -198,7 +200,7 @@ watch: { updateAspectRatio() { if (this.updateAspectRatio) { - this.graphHeight = 500; + this.graphHeight = 450; this.graphWidth = 600; this.measurements = measurements.large; this.draw(); @@ -216,14 +218,14 @@ <div :class="classType"> <h5 - class="text-center"> + class="text-center graph-title"> {{columnData.title}} </h5> - <div - class="prometheus-svg-container"> + <div + class="prometheus-svg-container" + :style="paddingBottomRootSvg"> <svg :viewBox="outterViewBox" - :style="{ 'padding-bottom': paddingBottomRootSvg }" ref="baseSvg"> <g class="x-axis" diff --git a/app/assets/javascripts/oauth_remember_me.js b/app/assets/javascripts/oauth_remember_me.js new file mode 100644 index 00000000000..ffc2dd6bbca --- /dev/null +++ b/app/assets/javascripts/oauth_remember_me.js @@ -0,0 +1,32 @@ +/** + * OAuth-based login buttons have a separate "remember me" checkbox. + * + * Toggling this checkbox adds/removes a `remember_me` parameter to the + * login buttons' href, which is passed on to the omniauth callback. + **/ + +export default class OAuthRememberMe { + constructor(opts = {}) { + this.container = opts.container || ''; + this.loginLinkSelector = '.oauth-login'; + } + + bindEvents() { + $('#remember_me', this.container).on('click', this.toggleRememberMe); + } + + // eslint-disable-next-line class-methods-use-this + toggleRememberMe(event) { + const rememberMe = $(event.target).is(':checked'); + + $('.oauth-login', this.container).each((i, element) => { + const href = $(element).attr('href'); + + if (rememberMe) { + $(element).attr('href', `${href}?remember_me=1`); + } else { + $(element).attr('href', href.replace('?remember_me=1', '')); + } + }); + } +} diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js index 46efdcf4202..5728afb4c59 100644 --- a/app/assets/javascripts/users_select.js +++ b/app/assets/javascripts/users_select.js @@ -206,8 +206,6 @@ function UsersSelect(currentUser, els) { return $dropdown.glDropdown({ showMenuAbove: showMenuAbove, data: function(term, callback) { - var isAuthorFilter; - isAuthorFilter = $('.js-author-search'); return _this.users(term, options, function(users) { // GitLabDropdownFilter returns this.instance // GitLabDropdownRemote returns this.options.instance diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index 245117b2559..c7c2684d548 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -17,6 +17,8 @@ max-width: $limited-layout-width-sm; margin-left: auto; margin-right: auto; + padding-top: 64px; + padding-bottom: 64px; } } diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index 9cff99b839c..23c06eca3c3 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -37,65 +37,77 @@ } .build-page { - .sticky { - position: absolute; - left: 0; - right: 0; + .build-trace-container { + position: relative; } - .build-trace-container { - position: absolute; - top: 225px; - left: 15px; - bottom: 10px; + .build-trace { background: $black; color: $gray-darkest; - font-family: $monospace_font; + white-space: pre; + overflow-x: auto; font-size: 12px; + border-radius: 0; + border: none; - &.sidebar-expanded { - right: 305px; + .bash { + display: block; } + } - &.sidebar-collapsed { - right: 16px; + .top-bar { + height: 35px; + display: flex; + justify-content: flex-end; + background: $gray-light; + border: 1px solid $border-color; + color: $gl-text-color; + position: sticky; + position: -webkit-sticky; + top: 50px; + + &.affix { + top: 50px; } - code { - background: $black; - color: $gray-darkest; + // with sidebar + &.affix.sidebar-expanded { + right: 306px; + left: 16px; } - .top-bar { - top: 0; - height: 35px; - display: flex; - justify-content: flex-end; - background: $gray-light; - border: 1px solid $border-color; - color: $gl-text-color; + // without sidebar + &.affix.sidebar-collapsed { + right: 16px; + left: 16px; + } - .truncated-info { - margin: 0 auto; - align-self: center; + &.affix-top { + position: absolute; + right: 0; + left: 0; + } - .truncated-info-size { - margin: 0 5px; - } + .truncated-info { + margin: 0 auto; + align-self: center; - .raw-link { - color: $gl-text-color; - margin-left: 5px; - text-decoration: underline; - } + .truncated-info-size { + margin: 0 5px; + } + + .raw-link { + color: $gl-text-color; + margin-left: 5px; + text-decoration: underline; } } .controllers { display: flex; - align-self: center; font-size: 15px; - margin-bottom: 4px; + justify-content: center; + align-items: center; svg { height: 15px; @@ -103,17 +115,9 @@ fill: $gl-text-color; } - .controllers-buttons, - .btn-scroll { - color: $gl-text-color; - height: 15px; - vertical-align: middle; - padding: 0; - width: 12px; - } - .controllers-buttons { - margin: 1px 10px; + color: $gl-text-color; + margin: 0 10px; } .btn-scroll.animate { @@ -143,15 +147,6 @@ } } - .bash { - top: 35px; - left: 10px; - bottom: 0; - padding: 10px 20px 20px 5px; - white-space: pre-wrap; - overflow: auto; - } - .environment-information { border: 1px solid $border-color; padding: 8px $gl-padding 12px; diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index a2be957655f..e9a679b20c2 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -254,7 +254,7 @@ .text-metric-usage { fill: $black; font-weight: 500; - font-size: 14px; + font-size: 12px; } .legend-axis-text { @@ -262,7 +262,11 @@ } .tick > text { - font-size: 14px; + font-size: 12px; + } + + .text-metric-title { + font-size: 12px; } @media (max-width: $screen-sm-max) { @@ -277,3 +281,9 @@ } } } + +.prometheus-row { + h5 { + font-size: 16px; + } +} diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index ba530bf7f9b..7d7c34115f9 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -483,11 +483,12 @@ a.deploy-project-label { .project-stats { font-size: 0; text-align: center; + max-width: 100%; + border-bottom: 1px solid $border-color; .nav { padding-top: 12px; padding-bottom: 12px; - border-bottom: 1px solid $border-color; } .nav > li { diff --git a/app/assets/stylesheets/pages/runners.scss b/app/assets/stylesheets/pages/runners.scss index 9b6ff237557..57c73295d1e 100644 --- a/app/assets/stylesheets/pages/runners.scss +++ b/app/assets/stylesheets/pages/runners.scss @@ -33,3 +33,20 @@ font-weight: normal; } } + +.admin-runner-btn-group-cell { + min-width: 150px; + + .btn-sm { + padding: 4px 9px; + } + + .btn-default { + color: $gl-text-color-secondary; + } + + .fa-pause, + .fa-play { + font-size: 11px; + } +} diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 824ce845706..b4c0cd0487f 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -110,6 +110,8 @@ class ApplicationController < ActionController::Base end def log_exception(exception) + Raven.capture_exception(exception) if sentry_enabled? + application_trace = ActionDispatch::ExceptionWrapper.new(env, exception).application_trace application_trace.map!{ |t| " #{t}\n" } logger.error "\n#{exception.class.name} (#{exception.message}):\n#{application_trace.join}" diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb index e52fa766044..6b1d418fc9a 100644 --- a/app/controllers/groups/milestones_controller.rb +++ b/app/controllers/groups/milestones_controller.rb @@ -11,6 +11,9 @@ class Groups::MilestonesController < Groups::ApplicationController @milestone_states = GlobalMilestone.states_count(@projects) @milestones = Kaminari.paginate_array(milestones).page(params[:page]) end + format.json do + render json: milestones.map { |m| m.for_display.slice(:title, :name) } + end end end diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb index b82681b197e..323d5d26eb6 100644 --- a/app/controllers/omniauth_callbacks_controller.rb +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -1,5 +1,6 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController include AuthenticatesWithTwoFactor + include Devise::Controllers::Rememberable protect_from_forgery except: [:kerberos, :saml, :cas3] @@ -115,8 +116,10 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController if @user.persisted? && @user.valid? log_audit_event(@user, with: oauth['provider']) if @user.two_factor_enabled? + params[:remember_me] = '1' if remember_me? prompt_for_two_factor(@user) else + remember_me(@user) if remember_me? sign_in_and_redirect(@user) end else @@ -147,4 +150,9 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController AuditEventService.new(user, user, options) .for_authentication.security_event end + + def remember_me? + request_params = request.env['omniauth.params'] + (request_params['remember_me'] == '1') if request_params.present? + end end diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index ca483c105b6..54f108353cd 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -227,7 +227,7 @@ class Projects::IssuesController < Projects::ApplicationController def issue return @issue if defined?(@issue) # The Sortable default scope causes performance issues when used with find_by - @noteable = @issue ||= @project.issues.where(iid: params[:id]).reorder(nil).take! + @noteable = @issue ||= @project.issues.find_by!(iid: params[:id]) return render_404 unless can?(current_user, :read_issue, @issue) diff --git a/app/finders/labels_finder.rb b/app/finders/labels_finder.rb index 042d792dada..ce432ddbfe6 100644 --- a/app/finders/labels_finder.rb +++ b/app/finders/labels_finder.rb @@ -83,7 +83,12 @@ class LabelsFinder < UnionFinder def projects return @projects if defined?(@projects) - @projects = skip_authorization ? Project.all : ProjectsFinder.new(current_user: current_user).execute + @projects = if skip_authorization + Project.all + else + ProjectsFinder.new(params: { non_archived: true }, current_user: current_user).execute + end + @projects = @projects.in_namespace(params[:group_id]) if group? @projects = @projects.where(id: params[:project_ids]) if projects? @projects = @projects.reorder(nil) diff --git a/app/finders/users_finder.rb b/app/finders/users_finder.rb index dbd50d1db7c..07deceb827b 100644 --- a/app/finders/users_finder.rb +++ b/app/finders/users_finder.rb @@ -60,13 +60,13 @@ class UsersFinder end def by_external_identity(users) - return users unless current_user.admin? && params[:extern_uid] && params[:provider] + return users unless current_user&.admin? && params[:extern_uid] && params[:provider] users.joins(:identities).merge(Identity.with_extern_uid(params[:provider], params[:extern_uid])) end def by_external(users) - return users = users.where.not(external: true) unless current_user.admin? + return users = users.where.not(external: true) unless current_user&.admin? return users unless params[:external] users.external diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 7be8e3b96cf..1c165700b19 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -298,10 +298,6 @@ module ApplicationHelper end end - def can_toggle_new_nav? - Rails.env.development? - end - def show_new_nav? cookies["new_nav"] == "true" end diff --git a/app/helpers/form_helper.rb b/app/helpers/form_helper.rb index 8ceb5c36bda..9247b1f72de 100644 --- a/app/helpers/form_helper.rb +++ b/app/helpers/form_helper.rb @@ -16,8 +16,8 @@ module FormHelper end end - def issue_dropdown_options(issuable, has_multiple_assignees = true) - options = { + def issue_assignees_dropdown_options + { toggle_class: 'js-user-search js-assignee-search js-multiselect js-save-user-data', title: 'Select assignee', filter: true, @@ -27,8 +27,8 @@ module FormHelper first_user: current_user&.username, null_user: true, current_user: true, - project_id: issuable.project.try(:id), - field_name: "#{issuable.class.model_name.param_key}[assignee_ids][]", + project_id: @project.id, + field_name: 'issue[assignee_ids][]', default_label: 'Unassigned', 'max-select': 1, 'dropdown-header': 'Assignee', @@ -38,13 +38,5 @@ module FormHelper current_user_info: current_user.to_json(only: [:id, :name]) } } - - if has_multiple_assignees - options[:title] = 'Select assignee(s)' - options[:data][:'dropdown-header'] = 'Assignee(s)' - options[:data].delete(:'max-select') - end - - options end end diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index af0b3e9c5bc..8cd61f738e1 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -58,6 +58,11 @@ module GroupsHelper IssuesFinder.new(current_user, group_id: group.id).execute end + def remove_group_message(group) + _("You are going to remove %{group_name}.\nRemoved groups CANNOT be restored!\nAre you ABSOLUTELY sure?") % + { group_name: group.name } + end + private def group_title_link(group, hidable: false) diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb index a230db22fa2..f346e20e807 100644 --- a/app/helpers/milestones_helper.rb +++ b/app/helpers/milestones_helper.rb @@ -74,6 +74,8 @@ module MilestonesHelper project = @target_project || @project if project namespace_project_milestones_path(project.namespace, project, :json) + elsif @group + group_milestones_path(@group, :json) else dashboard_milestones_path(:json) end diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index 8f15904f068..f39a3bb55a8 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -126,6 +126,18 @@ module SearchHelper search_path(options) end + def search_filter_input_options(type) + { + id: "filtered-search-#{type}", + placeholder: 'Search or filter results...', + data: { + 'project-id' => @project.id, + 'username-params' => @users.to_json(only: [:id, :username]), + 'base-endpoint' => namespace_project_path(@project.namespace, @project) + } + } + end + # Sanitize a HTML field for search display. Most tags are stripped out and the # maximum length is set to 200 characters. def search_md_sanitize(object, field) diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index a300536532b..2e7a80d308b 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -176,9 +176,12 @@ module Ci # * Lowercased # * Anything not matching [a-z0-9-] is replaced with a - # * Maximum length is 63 bytes + # * First/Last Character is not a hyphen def ref_slug - slugified = ref.to_s.downcase - slugified.gsub(/[^a-z0-9]/, '-')[0..62] + ref.to_s + .downcase + .gsub(/[^a-z0-9]/, '-')[0..62] + .gsub(/(\A-+|-+\z)/, '') end # Variables whose value does not depend on environment diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb index 96d6e120998..0b8d0ff881a 100644 --- a/app/models/ci/variable.rb +++ b/app/models/ci/variable.rb @@ -5,7 +5,7 @@ module Ci belongs_to :project - validates :key, uniqueness: { scope: :project_id } + validates :key, uniqueness: { scope: [:project_id, :environment_scope] } scope :unprotected, -> { where(protected: false) } end diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index d178ee4422b..41c8b525273 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -102,6 +102,14 @@ module Issuable def locking_enabled? title_changed? || description_changed? end + + def allows_multiple_assignees? + false + end + + def has_multiple_assignees? + assignees.count > 1 + end end module ClassMethods diff --git a/app/models/concerns/routable.rb b/app/models/concerns/routable.rb index ec7796a9dbb..ee108f010a6 100644 --- a/app/models/concerns/routable.rb +++ b/app/models/concerns/routable.rb @@ -103,8 +103,12 @@ module Routable def full_path return uncached_full_path unless RequestStore.active? - key = "routable/full_path/#{self.class.name}/#{self.id}" - RequestStore[key] ||= uncached_full_path + RequestStore[full_path_key] ||= uncached_full_path + end + + def expires_full_path_cache + RequestStore.delete(full_path_key) if RequestStore.active? + @full_path = nil end def build_full_path @@ -135,6 +139,10 @@ module Routable path_changed? || parent_changed? end + def full_path_key + @full_path_key ||= "routable/full_path/#{self.class.name}/#{self.id}" + end + def build_full_name if parent && name parent.human_name + ' / ' + name diff --git a/app/models/concerns/sortable.rb b/app/models/concerns/sortable.rb index a155a064032..fdacfa5a194 100644 --- a/app/models/concerns/sortable.rb +++ b/app/models/concerns/sortable.rb @@ -5,6 +5,25 @@ module Sortable extend ActiveSupport::Concern + module DropDefaultScopeOnFinders + # Override these methods to drop the `ORDER BY id DESC` default scope. + # See http://dba.stackexchange.com/a/110919 for why we do this. + %i[find find_by find_by!].each do |meth| + define_method meth do |*args, &block| + return super(*args, &block) if block + + unordered_relation = unscope(:order) + + # We cannot simply call `meth` on `unscope(:order)`, since that is also + # an instance of the same relation class this module is included into, + # which means we'd get infinite recursion. + # We explicitly use the original implementation to prevent this. + original_impl = method(__method__).super_method.unbind + original_impl.bind(unordered_relation).call(*args) + end + end + end + included do # By default all models should be ordered # by created_at field starting from newest @@ -18,6 +37,10 @@ module Sortable scope :order_updated_asc, -> { reorder(updated_at: :asc) } scope :order_name_asc, -> { reorder(name: :asc) } scope :order_name_desc, -> { reorder(name: :desc) } + + # All queries (relations) on this model are instances of this `relation_klass`. + relation_klass = relation_delegate_class(ActiveRecord::Relation) + relation_klass.prepend DropDefaultScopeOnFinders end module ClassMethods diff --git a/app/models/forked_project_link.rb b/app/models/forked_project_link.rb index 36cf7ad6a28..8d35864eff6 100644 --- a/app/models/forked_project_link.rb +++ b/app/models/forked_project_link.rb @@ -1,4 +1,4 @@ class ForkedProjectLink < ActiveRecord::Base - belongs_to :forked_to_project, class_name: 'Project' - belongs_to :forked_from_project, class_name: 'Project' + belongs_to :forked_to_project, -> { where.not(pending_delete: true) }, class_name: 'Project' + belongs_to :forked_from_project, -> { where.not(pending_delete: true) }, class_name: 'Project' end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index c099d731082..808212c780c 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -197,11 +197,19 @@ class MergeRequest < ActiveRecord::Base } end - # This method is needed for compatibility with issues to not mess view and other code + # These method are needed for compatibility with issues to not mess view and other code def assignees Array(assignee) end + def assignee_ids + Array(assignee_id) + end + + def assignee_ids=(ids) + write_attribute(:assignee_id, ids.last) + end + def assignee_or_author?(user) author_id == user.id || assignee_id == user.id end diff --git a/app/models/project.rb b/app/models/project.rb index 96400b07b18..f75eedcbdff 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -825,7 +825,7 @@ class Project < ActiveRecord::Base end def ci_service - @ci_service ||= ci_services.reorder(nil).find_by(active: true) + @ci_service ||= ci_services.find_by(active: true) end def deployment_services @@ -833,7 +833,7 @@ class Project < ActiveRecord::Base end def deployment_service - @deployment_service ||= deployment_services.reorder(nil).find_by(active: true) + @deployment_service ||= deployment_services.find_by(active: true) end def monitoring_services @@ -841,7 +841,7 @@ class Project < ActiveRecord::Base end def monitoring_service - @monitoring_service ||= monitoring_services.reorder(nil).find_by(active: true) + @monitoring_service ||= monitoring_services.find_by(active: true) end def jira_tracker? @@ -973,6 +973,7 @@ class Project < ActiveRecord::Base begin gitlab_shell.mv_repository(repository_storage_path, "#{old_path_with_namespace}.wiki", "#{new_path_with_namespace}.wiki") send_move_instructions(old_path_with_namespace) + expires_full_path_cache @old_path_with_namespace = old_path_with_namespace @@ -1084,21 +1085,21 @@ class Project < ActiveRecord::Base merge_requests.where(source_project_id: self.id) end - def create_repository + def create_repository(force: false) # Forked import is handled asynchronously - unless forked? - if gitlab_shell.add_repository(repository_storage_path, path_with_namespace) - repository.after_create - true - else - errors.add(:base, 'Failed to create repository via gitlab-shell') - false - end + return if forked? && !force + + if gitlab_shell.add_repository(repository_storage_path, path_with_namespace) + repository.after_create + true + else + errors.add(:base, 'Failed to create repository via gitlab-shell') + false end end def ensure_repository - create_repository unless repository_exists? + create_repository(force: true) unless repository_exists? end def repository_exists? diff --git a/app/policies/base_policy.rb b/app/policies/base_policy.rb index 191c2e78a08..a605a3457c8 100644 --- a/app/policies/base_policy.rb +++ b/app/policies/base_policy.rb @@ -1,6 +1,8 @@ require_dependency 'declarative_policy' class BasePolicy < DeclarativePolicy::Base + include Gitlab::CurrentSettings + desc "User is an instance admin" with_options scope: :user, score: 0 condition(:admin) { @user&.admin? } @@ -10,4 +12,9 @@ class BasePolicy < DeclarativePolicy::Base with_options scope: :user, score: 0 condition(:can_create_group) { @user&.can_create_group } + + desc "The application is restricted from public visibility" + condition(:restricted_public_level, scope: :global) do + current_application_settings.restricted_visibility_levels.include?(Gitlab::VisibilityLevel::PUBLIC) + end end diff --git a/app/policies/global_policy.rb b/app/policies/global_policy.rb index 535faa922dd..55eefa76d3f 100644 --- a/app/policies/global_policy.rb +++ b/app/policies/global_policy.rb @@ -11,10 +11,16 @@ class GlobalPolicy < BasePolicy with_options scope: :user, score: 0 condition(:access_locked) { @user.access_locked? } - rule { anonymous }.prevent_all + rule { anonymous }.policy do + prevent :log_in + prevent :access_api + prevent :access_git + prevent :receive_notifications + prevent :use_quick_actions + prevent :create_group + end rule { default }.policy do - enable :read_users_list enable :log_in enable :access_api enable :access_git @@ -37,4 +43,8 @@ class GlobalPolicy < BasePolicy rule { access_locked }.policy do prevent :log_in end + + rule { ~restricted_public_level }.policy do + enable :read_users_list + end end diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb index 0181ddf85e0..0905ddd9b38 100644 --- a/app/policies/user_policy.rb +++ b/app/policies/user_policy.rb @@ -1,11 +1,4 @@ class UserPolicy < BasePolicy - include Gitlab::CurrentSettings - - desc "The application is restricted from public visibility" - condition(:restricted_public_level, scope: :global) do - current_application_settings.restricted_visibility_levels.include?(Gitlab::VisibilityLevel::PUBLIC) - end - desc "The current user is the user in question" condition(:user_is_self, score: 0) { @subject == @user } diff --git a/app/services/access_token_validation_service.rb b/app/services/access_token_validation_service.rb index b2a543daa00..9c00ea789ec 100644 --- a/app/services/access_token_validation_service.rb +++ b/app/services/access_token_validation_service.rb @@ -5,10 +5,11 @@ class AccessTokenValidationService REVOKED = :revoked INSUFFICIENT_SCOPE = :insufficient_scope - attr_reader :token + attr_reader :token, :request - def initialize(token) + def initialize(token, request: nil) @token = token + @request = request end def validate(scopes: []) @@ -27,12 +28,23 @@ class AccessTokenValidationService end # True if the token's scope contains any of the passed scopes. - def include_any_scope?(scopes) - if scopes.blank? + def include_any_scope?(required_scopes) + if required_scopes.blank? true else - # Check whether the token is allowed access to any of the required scopes. - Set.new(scopes).intersection(Set.new(token.scopes)).present? + # We're comparing each required_scope against all token scopes, which would + # take quadratic time. This consideration is irrelevant here because of the + # small number of records involved. + # https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12300/#note_33689006 + token_scopes = token.scopes.map(&:to_sym) + + required_scopes.any? do |scope| + if scope.respond_to?(:sufficient?) + scope.sufficient?(token_scopes, request) + else + API::Scope.new(scope).sufficient?(token_scopes, request) + end + end end end end diff --git a/app/services/delete_merged_branches_service.rb b/app/services/delete_merged_branches_service.rb index 3b611588466..5c9e2a16c71 100644 --- a/app/services/delete_merged_branches_service.rb +++ b/app/services/delete_merged_branches_service.rb @@ -10,6 +10,8 @@ class DeleteMergedBranchesService < BaseService branches = branches.select { |branch| project.repository.merged_to_root_ref?(branch) } # Prevent deletion of branches relevant to open merge requests branches -= merge_request_branch_names + # Prevent deletion of protected branches + branches -= project.protected_branches.pluck(:name) branches.each do |branch| DeleteBranchService.new(project, current_user).execute(branch) diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb index b247cb89e5e..bc846e07f24 100644 --- a/app/services/merge_requests/merge_service.rb +++ b/app/services/merge_requests/merge_service.rb @@ -61,8 +61,12 @@ module MergeRequests MergeRequests::PostMergeService.new(project, current_user).execute(merge_request) if params[:should_remove_source_branch].present? || @merge_request.force_remove_source_branch? - DeleteBranchService.new(@merge_request.source_project, branch_deletion_user) - .execute(merge_request.source_branch) + # Verify again that the source branch can be removed, since branch may be protected, + # or the source branch may have been updated. + if @merge_request.can_remove_source_branch?(branch_deletion_user) + DeleteBranchService.new(@merge_request.source_project, branch_deletion_user) + .execute(merge_request.source_branch) + end end end diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb index fd701e33524..4bb98e5cb4e 100644 --- a/app/services/projects/transfer_service.rb +++ b/app/services/projects/transfer_service.rb @@ -78,6 +78,7 @@ module Projects Gitlab::PagesTransfer.new.move_project(project.path, @old_namespace.full_path, @new_namespace.full_path) project.old_path_with_namespace = @old_path + project.expires_full_path_cache execute_system_hooks end diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb index 6816b137361..e4dfe87e614 100644 --- a/app/services/quick_actions/interpret_service.rb +++ b/app/services/quick_actions/interpret_service.rb @@ -92,9 +92,12 @@ module QuickActions desc 'Assign' explanation do |users| - "Assigns #{users.first.to_reference}." if users.any? + users = issuable.allows_multiple_assignees? ? users : users.take(1) + "Assigns #{users.map(&:to_reference).to_sentence}." + end + params do + issuable.allows_multiple_assignees? ? '@user1 @user2' : '@user' end - params '@user' condition do current_user.can?(:"admin_#{issuable.to_ability_name}", project) end @@ -104,28 +107,69 @@ module QuickActions command :assign do |users| next if users.empty? - if issuable.is_a?(Issue) - @updates[:assignee_ids] = [users.last.id] + @updates[:assignee_ids] = + if issuable.allows_multiple_assignees? + issuable.assignees.pluck(:id) + users.map(&:id) + else + [users.last.id] + end + end + + desc do + if issuable.allows_multiple_assignees? + 'Remove all or specific assignee(s)' else - @updates[:assignee_id] = users.last.id + 'Remove assignee' end end - - desc 'Remove assignee' explanation do - "Removes assignee #{issuable.assignees.first.to_reference}." + "Removes #{'assignee'.pluralize(issuable.assignees.size)} #{issuable.assignees.map(&:to_reference).to_sentence}." + end + params do + issuable.allows_multiple_assignees? ? '@user1 @user2' : '' end condition do issuable.persisted? && issuable.assignees.any? && current_user.can?(:"admin_#{issuable.to_ability_name}", project) end - command :unassign do - if issuable.is_a?(Issue) - @updates[:assignee_ids] = [] - else - @updates[:assignee_id] = nil - end + parse_params do |unassign_param| + # When multiple users are assigned, all will be unassigned if multiple assignees are no longer allowed + extract_users(unassign_param) if issuable.allows_multiple_assignees? + end + command :unassign do |users = nil| + @updates[:assignee_ids] = + if users&.any? + issuable.assignees.pluck(:id) - users.map(&:id) + else + [] + end + end + + desc do + "Change assignee#{'(s)' if issuable.allows_multiple_assignees?}" + end + explanation do |users| + users = issuable.allows_multiple_assignees? ? users : users.take(1) + "Change #{'assignee'.pluralize(users.size)} to #{users.map(&:to_reference).to_sentence}." + end + params do + issuable.allows_multiple_assignees? ? '@user1 @user2' : '@user' + end + condition do + issuable.persisted? && + current_user.can?(:"admin_#{issuable.to_ability_name}", project) + end + parse_params do |assignee_param| + extract_users(assignee_param) + end + command :reassign do |users| + @updates[:assignee_ids] = + if issuable.allows_multiple_assignees? + users.map(&:id) + else + [users.last.id] + end end desc 'Set milestone' diff --git a/app/views/admin/runners/_runner.html.haml b/app/views/admin/runners/_runner.html.haml index d4d166ab7b6..140688b52d3 100644 --- a/app/views/admin/runners/_runner.html.haml +++ b/app/views/admin/runners/_runner.html.haml @@ -32,13 +32,16 @@ #{time_ago_in_words(runner.contacted_at)} ago - else Never - %td - .pull-right - = link_to 'Edit', admin_runner_path(runner), class: 'btn btn-sm' + %td.admin-runner-btn-group-cell + .pull-right.btn-group + = link_to admin_runner_path(runner), class: 'btn btn-sm btn-default has-tooltip', title: 'Edit', ref: 'tooltip', aria: { label: 'Edit' }, data: { placement: 'top', container: 'body'} do + = icon('pencil') - if runner.active? - = link_to 'Pause', [:pause, :admin, runner], data: { confirm: "Are you sure?" }, method: :get, class: 'btn btn-danger btn-sm' + = link_to [:pause, :admin, runner], method: :get, class: 'btn btn-sm btn-default has-tooltip', title: 'Pause', ref: 'tooltip', aria: { label: 'Pause' }, data: { placement: 'top', container: 'body', confirm: "Are you sure?" } do + = icon('pause') - else - = link_to 'Resume', [:resume, :admin, runner], method: :get, class: 'btn btn-success btn-sm' - = link_to 'Remove', [:admin, runner], data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm' - + = link_to [:resume, :admin, runner], method: :get, class: 'btn btn-default btn-sm has-tooltip', title: 'Resume', ref: 'tooltip', aria: { label: 'Resume' }, data: { placement: 'top', container: 'body'} do + = icon('play') + = link_to [:admin, runner], method: :delete, class: 'btn btn-danger btn-sm has-tooltip', title: 'Remove', ref: 'tooltip', aria: { label: 'Remove' }, data: { placement: 'top', container: 'body', confirm: "Are you sure?" } do + = icon('remove') diff --git a/app/views/devise/shared/_omniauth_box.html.haml b/app/views/devise/shared/_omniauth_box.html.haml index f92f89e73ff..e80d10dc8f1 100644 --- a/app/views/devise/shared/_omniauth_box.html.haml +++ b/app/views/devise/shared/_omniauth_box.html.haml @@ -6,4 +6,7 @@ - providers.each do |provider| %span.light - has_icon = provider_has_icon?(provider) - = link_to provider_image_tag(provider), omniauth_authorize_path(:user, provider), method: :post, class: (has_icon ? 'oauth-image-link' : 'btn') + = link_to provider_image_tag(provider), omniauth_authorize_path(:user, provider), method: :post, class: 'oauth-login' + (has_icon ? ' oauth-image-link' : ' btn'), id: "oauth-login-#{provider}" + %fieldset + = check_box_tag :remember_me + = label_tag :remember_me, 'Remember Me' diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml index d696577278d..298604dee8c 100644 --- a/app/views/devise/shared/_signup_box.html.haml +++ b/app/views/devise/shared/_signup_box.html.haml @@ -25,7 +25,7 @@ %div - if Gitlab::Recaptcha.enabled? = recaptcha_tags - %div + .submit-container = f.submit "Register", class: "btn-register btn" .clearfix.submit-container %p diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml index 7d5add3cc1c..9ebb3894c55 100644 --- a/app/views/groups/edit.html.haml +++ b/app/views/groups/edit.html.haml @@ -45,10 +45,13 @@ .panel.panel-danger .panel-heading Remove group .panel-body - %p - Removing group will cause all child projects and resources to be removed. - %br - %strong Removed group can not be restored! + = form_tag(@group, method: :delete) do + %p + Removing group will cause all child projects and resources to be removed. + %br + %strong Removed group can not be restored! - .form-actions - = link_to 'Remove group', @group, data: {confirm: 'Removed group can not be restored! Are you sure?'}, method: :delete, class: "btn btn-remove" + .form-actions + = button_to 'Remove group', '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_group_message(@group) } + += render 'shared/confirm_modal', phrase: @group.path diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 8cbc3f6105f..7e8b9cb9ad0 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -74,9 +74,8 @@ = link_to "Profile", current_user, class: 'profile-link', data: { user: current_user.username } %li = link_to "Settings", profile_path - - if can_toggle_new_nav? - %li - = link_to "Turn on new nav", profile_preferences_path(anchor: "new-navigation") + %li + = link_to "Turn on new nav", profile_preferences_path(anchor: "new-navigation") %li.divider %li = link_to "Sign out", destroy_user_session_path, method: :delete, class: "sign-out-link" diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml index a089aeb2447..bd602071384 100644 --- a/app/views/profiles/preferences/show.html.haml +++ b/app/views/profiles/preferences/show.html.haml @@ -16,25 +16,22 @@ .preview= image_tag "#{scheme.css_class}-scheme-preview.png" = f.radio_button :color_scheme_id, scheme.id = scheme.name - - if can_toggle_new_nav? - .col-sm-12 - %hr - .col-lg-3.profile-settings-sidebar#new-navigation - %h4.prepend-top-0 - New Navigation - %p - This setting allows you to turn on or off the new upcoming navigation concept. - = succeed '.' do - = link_to 'Learn more', '', target: '_blank' - .col-lg-9.syntax-theme - = label_tag do - .preview= image_tag "old_nav.png" - %input.js-experiment-feature-toggle{ type: "radio", value: "false", name: "new_nav", checked: !show_new_nav? } - Old - = label_tag do - .preview= image_tag "new_nav.png" - %input.js-experiment-feature-toggle{ type: "radio", value: "true", name: "new_nav", checked: show_new_nav? } - New + .col-sm-12 + %hr + .col-lg-4.profile-settings-sidebar#new-navigation + %h4.prepend-top-0 + New Navigation + %p + This setting allows you to turn on or off the new upcoming navigation concept. + .col-lg-8.syntax-theme + = label_tag do + .preview= image_tag "old_nav.png" + %input.js-experiment-feature-toggle{ type: "radio", value: "false", name: "new_nav", checked: !show_new_nav? } + Old + = label_tag do + .preview= image_tag "new_nav.png" + %input.js-experiment-feature-toggle{ type: "radio", value: "true", name: "new_nav", checked: show_new_nav? } + New .col-sm-12 %hr .col-lg-4.profile-settings-sidebar diff --git a/app/views/projects/boards/components/_sidebar.html.haml b/app/views/projects/boards/components/_sidebar.html.haml index 24d76da6f06..09d70f658a3 100644 --- a/app/views/projects/boards/components/_sidebar.html.haml +++ b/app/views/projects/boards/components/_sidebar.html.haml @@ -23,4 +23,5 @@ = render "projects/boards/components/sidebar/labels" = render "projects/boards/components/sidebar/notifications" %remove-btn{ ":issue" => "issue", - ":list" => "list" } + ":list" => "list", + "v-if" => "canRemove" } diff --git a/app/views/projects/boards/components/sidebar/_assignee.html.haml b/app/views/projects/boards/components/sidebar/_assignee.html.haml index e8db868f49b..c314573bdea 100644 --- a/app/views/projects/boards/components/sidebar/_assignee.html.haml +++ b/app/views/projects/boards/components/sidebar/_assignee.html.haml @@ -19,10 +19,11 @@ ":data-name" => "assignee.name", ":data-username" => "assignee.username" } .dropdown - %button.dropdown-menu-toggle.js-user-search.js-author-search.js-multiselect.js-save-user-data.js-issue-board-sidebar{ type: "button", ref: "assigneeDropdown", data: { toggle: "dropdown", field_name: "issue[assignee_ids][]", first_user: (current_user.username if current_user), current_user: "true", project_id: @project.id, null_user: "true", multi_select: "true", 'max-select' => 1, dropdown: { header: 'Assignee' } }, + - dropdown_options = issue_assignees_dropdown_options + %button.dropdown-menu-toggle.js-user-search.js-author-search.js-multiselect.js-save-user-data.js-issue-board-sidebar{ type: 'button', ref: 'assigneeDropdown', data: { toggle: 'dropdown', field_name: 'issue[assignee_ids][]', first_user: current_user&.username, current_user: 'true', project_id: @project.id, null_user: 'true', multi_select: 'true', 'dropdown-header': dropdown_options[:data][:'dropdown-header'], 'max-select': dropdown_options[:data][:'max-select'] }, ":data-issuable-id" => "issue.id", ":data-issue-update" => "'#{namespace_project_issues_path(@project.namespace, @project)}/' + issue.id + '.json'" } - Select assignee + = dropdown_options[:title] = icon("chevron-down") .dropdown-menu.dropdown-select.dropdown-menu-user.dropdown-menu-selectable.dropdown-menu-author = dropdown_title("Assign to") diff --git a/app/views/projects/environments/metrics.html.haml b/app/views/projects/environments/metrics.html.haml index c5722cf5997..3aa41174b74 100644 --- a/app/views/projects/environments/metrics.html.haml +++ b/app/views/projects/environments/metrics.html.haml @@ -10,7 +10,7 @@ .top-area .row .col-sm-6 - %h3.page-title + %h3 Environment: = link_to @environment.name, environment_path(@environment) diff --git a/app/views/projects/jobs/show.html.haml b/app/views/projects/jobs/show.html.haml index c73bae0a2c9..e9bc1068417 100644 --- a/app/views/projects/jobs/show.html.haml +++ b/app/views/projects/jobs/show.html.haml @@ -54,13 +54,14 @@ - else Job has been erased #{time_ago_with_tooltip(@build.erased_at)} - .build-trace-container#build-trace - .top-bar.sticky + .build-trace-container.prepend-top-default + .top-bar.js-top-bar .js-truncated-info.truncated-info.hidden< Showing last %span.js-truncated-info-size.truncated-info-size>< KiB of log - %a.js-raw-link.raw-link{ href: raw_namespace_project_job_path(@project.namespace, @project, @build) }>< Complete Raw + .controllers - if @build.has_trace? = link_to raw_namespace_project_job_path(@project.namespace, @project, @build), @@ -82,10 +83,12 @@ .has-tooltip.controllers-buttons{ title: 'Scroll to bottom', data: { placement: 'top', container: 'body'} } %button.js-scroll-down.btn-scroll.btn-transparent.btn-blank{ type: 'button', disabled: true } = custom_icon('scroll_down') - .bash.sticky.js-scroll-container - %code.js-build-output + + %pre.build-trace#build-trace + %code.bash.js-build-output .build-loader-animation.js-build-refresh + = render "sidebar" .js-build-options{ data: javascript_build_options } diff --git a/app/views/projects/pipelines/charts.html.haml b/app/views/projects/pipelines/charts.html.haml index 8ffddfe6154..78002e8cd64 100644 --- a/app/views/projects/pipelines/charts.html.haml +++ b/app/views/projects/pipelines/charts.html.haml @@ -1,5 +1,5 @@ - @no_container = true -- page_title "Charts", "Pipelines" +- page_title _("Charts"), _("Pipelines") - content_for :page_specific_javascripts do = page_specific_javascript_bundle_tag('common_d3') = page_specific_javascript_bundle_tag('graphs') @@ -8,7 +8,7 @@ %div{ class: container_class } .sub-header-block .oneline - A collection of graphs for Continuous Integration + = _("A collection of graphs regarding Continuous Integration") #charts.ci-charts .row diff --git a/app/views/projects/pipelines/charts/_overall.haml b/app/views/projects/pipelines/charts/_overall.haml index 93083397d5b..66786c7ff59 100644 --- a/app/views/projects/pipelines/charts/_overall.haml +++ b/app/views/projects/pipelines/charts/_overall.haml @@ -1,15 +1,15 @@ -%h4 Overall stats +%h4= s_("PipelineCharts|Overall statistics") %ul %li - Total: - %strong= pluralize @counts[:total], 'pipeline' + = s_("PipelineCharts|Total:") + %strong= n_("1 pipeline", "%d pipelines", @counts[:total]) % @counts[:total] %li - Successful: - %strong= pluralize @counts[:success], 'pipeline' + = s_("PipelineCharts|Successful:") + %strong= n_("1 pipeline", "%d pipelines", @counts[:success]) % @counts[:success] %li - Failed: - %strong= pluralize @counts[:failed], 'pipeline' + = s_("PipelineCharts|Failed:") + %strong= n_("1 pipeline", "%d pipelines", @counts[:failed]) % @counts[:failed] %li - Success ratio: + = s_("PipelineCharts|Success ratio:") %strong #{success_ratio(@counts)}% diff --git a/app/views/projects/pipelines/charts/_pipeline_times.haml b/app/views/projects/pipelines/charts/_pipeline_times.haml index aee7c5492aa..1292f580a81 100644 --- a/app/views/projects/pipelines/charts/_pipeline_times.haml +++ b/app/views/projects/pipelines/charts/_pipeline_times.haml @@ -1,6 +1,6 @@ %div %p.light - Commit duration in minutes for last 30 commits + = _("Commit duration in minutes for last 30 commits") %canvas#build_timesChart{ height: 200 } diff --git a/app/views/projects/pipelines/charts/_pipelines.haml b/app/views/projects/pipelines/charts/_pipelines.haml index b6f453b9736..be884448087 100644 --- a/app/views/projects/pipelines/charts/_pipelines.haml +++ b/app/views/projects/pipelines/charts/_pipelines.haml @@ -1,29 +1,29 @@ -%h4 Pipelines charts +%h4= _("Pipelines charts") %p %span.cgreen = icon("circle") - success + = s_("Pipeline|success") %span.cgray = icon("circle") - all + = s_("Pipeline|all") .prepend-top-default %p.light - Jobs for last week + = _("Jobs for last week") (#{date_from_to(Date.today - 7.days, Date.today)}) %canvas#weekChart{ height: 200 } .prepend-top-default %p.light - Jobs for last month + = _("Jobs for last month") (#{date_from_to(Date.today - 30.days, Date.today)}) %canvas#monthChart{ height: 200 } .prepend-top-default %p.light - Jobs for last year + = _("Jobs for last year") %canvas#yearChart.padded{ height: 250 } - [:week, :month, :year].each do |scope| diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index e1e70a53709..152e50a79bb 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -73,7 +73,7 @@ = link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml', commit_message: 'Set up auto deploy', branch_name: 'auto-deploy', context: 'autodeploy') do #{ _('Set up auto deploy') } -%div{ class: container_class } +%div{ class: [container_class, ("limit-container-width" unless fluid_layout)] } - if @project.archived? .text-warning.center.prepend-top-20 %p diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index d3d290692a2..ae890567225 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -23,7 +23,7 @@ .scroll-container %ul.tokens-container.list-unstyled %li.input-token - %input.form-control.filtered-search{ id: "filtered-search-#{type.to_s}", placeholder: 'Search or filter results...', data: { 'project-id' => @project.id, 'username-params' => @users.to_json(only: [:id, :username]), 'base-endpoint' => namespace_project_path(@project.namespace, @project) } } + %input.form-control.filtered-search{ search_filter_input_options(type) } = icon('filter') #js-dropdown-hint.filtered-search-input-dropdown-menu.dropdown-menu.hint-dropdown %ul{ data: { dropdown: true } } diff --git a/app/views/shared/issuable/_sidebar_assignees.html.haml b/app/views/shared/issuable/_sidebar_assignees.html.haml index 2ea5eb960c0..57392cd7fbb 100644 --- a/app/views/shared/issuable/_sidebar_assignees.html.haml +++ b/app/views/shared/issuable/_sidebar_assignees.html.haml @@ -37,19 +37,20 @@ - issuable.assignees.each do |assignee| = hidden_field_tag "#{issuable.to_ability_name}[assignee_ids][]", assignee.id, id: nil, data: { avatar_url: assignee.avatar_url, name: assignee.name, username: assignee.username } - - options = { toggle_class: 'js-user-search js-author-search', title: 'Assign to', filter: true, dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author', placeholder: 'Search users', data: { first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), author_id: issuable.author_id, field_name: "#{issuable.to_ability_name}[assignee_ids][]", issue_update: issuable_json_path(issuable), ability_name: issuable.to_ability_name, null_user: true } } - + - options = { toggle_class: 'js-user-search js-author-search', title: 'Assign to', filter: true, dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author', placeholder: 'Search users', data: { first_user: current_user&.username, current_user: true, project_id: @project&.id, author_id: issuable.author_id, field_name: "#{issuable.to_ability_name}[assignee_ids][]", issue_update: issuable_json_path(issuable), ability_name: issuable.to_ability_name, null_user: true } } - title = 'Select assignee' - if issuable.is_a?(Issue) - unless issuable.assignees.any? = hidden_field_tag "#{issuable.to_ability_name}[assignee_ids][]", 0, id: nil + - dropdown_options = issue_assignees_dropdown_options + - title = dropdown_options[:title] - options[:toggle_class] += ' js-multiselect js-save-user-data' - data = { field_name: "#{issuable.to_ability_name}[assignee_ids][]" } - data[:multi_select] = true - data['dropdown-title'] = title - - data['dropdown-header'] = 'Assignee' - - data['max-select'] = 1 + - data['dropdown-header'] = dropdown_options[:data][:'dropdown-header'] + - data['max-select'] = dropdown_options[:data][:'max-select'] - options[:data].merge!(data) = dropdown_tag(title, options: options) diff --git a/app/views/shared/issuable/form/_merge_params.html.haml b/app/views/shared/issuable/form/_merge_params.html.haml index bfa91629e1e..8f6509a8ce8 100644 --- a/app/views/shared/issuable/form/_merge_params.html.haml +++ b/app/views/shared/issuable/form/_merge_params.html.haml @@ -11,8 +11,7 @@ .col-sm-10.col-sm-offset-2 - if issuable.can_remove_source_branch?(current_user) .checkbox - - initial_checkbox_value = issuable.merge_params.key?('force_remove_source_branch') ? issuable.force_remove_source_branch? : true = label_tag 'merge_request[force_remove_source_branch]' do = hidden_field_tag 'merge_request[force_remove_source_branch]', '0', id: nil - = check_box_tag 'merge_request[force_remove_source_branch]', '1', initial_checkbox_value + = check_box_tag 'merge_request[force_remove_source_branch]', '1', issuable.force_remove_source_branch? Remove source branch when merge request is accepted. diff --git a/app/views/shared/issuable/form/_metadata_issue_assignee.html.haml b/app/views/shared/issuable/form/_metadata_issue_assignee.html.haml index 77175c839a6..567cde764e2 100644 --- a/app/views/shared/issuable/form/_metadata_issue_assignee.html.haml +++ b/app/views/shared/issuable/form/_metadata_issue_assignee.html.haml @@ -7,5 +7,5 @@ - if issuable.assignees.length === 0 = hidden_field_tag "#{issuable.to_ability_name}[assignee_ids][]", 0, id: nil, data: { meta: '' } - = dropdown_tag(users_dropdown_label(issuable.assignees), options: issue_dropdown_options(issuable,false)) + = dropdown_tag(users_dropdown_label(issuable.assignees), options: issue_assignees_dropdown_options) = link_to 'Assign to me', '#', class: "assign-to-me-link #{'hide' if issuable.assignees.include?(current_user)}" diff --git a/app/views/shared/members/_access_request_buttons.html.haml b/app/views/shared/members/_access_request_buttons.html.haml index d97fdf179d7..40224cec9e8 100644 --- a/app/views/shared/members/_access_request_buttons.html.haml +++ b/app/views/shared/members/_access_request_buttons.html.haml @@ -1,18 +1,20 @@ - model_name = source.model_name.to_s.downcase -.project-action-button.inline - - if can?(current_user, :"destroy_#{model_name}_member", source.members.find_by(user_id: current_user.id)) +- if can?(current_user, :"destroy_#{model_name}_member", source.members.find_by(user_id: current_user.id)) + .project-action-button.inline - link_text = source.is_a?(Group) ? _('Leave group') : _('Leave project') = link_to link_text, polymorphic_path([:leave, source, :members]), method: :delete, data: { confirm: leave_confirmation_message(source) }, class: 'btn' - - elsif requester = source.requesters.find_by(user_id: current_user.id) +- elsif requester = source.requesters.find_by(user_id: current_user.id) + .project-action-button.inline = link_to _('Withdraw Access Request'), polymorphic_path([:leave, source, :members]), method: :delete, data: { confirm: remove_member_message(requester) }, class: 'btn' - - elsif source.request_access_enabled && can?(current_user, :request_access, source) +- elsif source.request_access_enabled && can?(current_user, :request_access, source) + .project-action-button.inline = link_to _('Request Access'), polymorphic_path([:request_access, source, :members]), method: :post, class: 'btn' diff --git a/changelogs/unreleased/18000-remember-me-for-oauth-login.yml b/changelogs/unreleased/18000-remember-me-for-oauth-login.yml new file mode 100644 index 00000000000..1ef92756a76 --- /dev/null +++ b/changelogs/unreleased/18000-remember-me-for-oauth-login.yml @@ -0,0 +1,4 @@ +--- +title: Honor the "Remember me" parameter for OAuth-based login +merge_request: 11963 +author: diff --git a/changelogs/unreleased/23036-replace-dashboard-new-project-spinach.yml b/changelogs/unreleased/23036-replace-dashboard-new-project-spinach.yml new file mode 100644 index 00000000000..a5f78202c93 --- /dev/null +++ b/changelogs/unreleased/23036-replace-dashboard-new-project-spinach.yml @@ -0,0 +1,4 @@ +--- +title: Replace 'dashboard/new-project.feature' spinach with rspec +merge_request: 12550 +author: Alexander Randa (@randaalex) diff --git a/changelogs/unreleased/32048-shared-runners-admin-buttons-have-odd-spacing.yml b/changelogs/unreleased/32048-shared-runners-admin-buttons-have-odd-spacing.yml new file mode 100644 index 00000000000..99e64b9b467 --- /dev/null +++ b/changelogs/unreleased/32048-shared-runners-admin-buttons-have-odd-spacing.yml @@ -0,0 +1,4 @@ +--- +title: Fix spacing on runner buttons. +merge_request: !12535 +author: diff --git a/changelogs/unreleased/32885-unintentionally-removing-branch-when-merging-merge-request.yml b/changelogs/unreleased/32885-unintentionally-removing-branch-when-merging-merge-request.yml new file mode 100644 index 00000000000..313aeab91b5 --- /dev/null +++ b/changelogs/unreleased/32885-unintentionally-removing-branch-when-merging-merge-request.yml @@ -0,0 +1,4 @@ +--- +title: Set default for Remove source branch to false. +merge_request: !12576 +author: diff --git a/changelogs/unreleased/33130-remove-group-modal.yml b/changelogs/unreleased/33130-remove-group-modal.yml new file mode 100644 index 00000000000..4672d41ded5 --- /dev/null +++ b/changelogs/unreleased/33130-remove-group-modal.yml @@ -0,0 +1,4 @@ +--- +title: "Remove group modal like remove project modal (requires typing + confirmation)" +merge_request: 12569 +author: Diego Souza diff --git a/changelogs/unreleased/33580-fix-api-scoping.yml b/changelogs/unreleased/33580-fix-api-scoping.yml new file mode 100644 index 00000000000..f4ebb13c082 --- /dev/null +++ b/changelogs/unreleased/33580-fix-api-scoping.yml @@ -0,0 +1,4 @@ +--- +title: Fix API Scoping +merge_request: 12300 +author: diff --git a/changelogs/unreleased/34097-issue-board-remove-from-board-button-when-viewing-an-issue-gives-js-error-and-fails.yml b/changelogs/unreleased/34097-issue-board-remove-from-board-button-when-viewing-an-issue-gives-js-error-and-fails.yml new file mode 100644 index 00000000000..c0ea75adfa0 --- /dev/null +++ b/changelogs/unreleased/34097-issue-board-remove-from-board-button-when-viewing-an-issue-gives-js-error-and-fails.yml @@ -0,0 +1,4 @@ +--- +title: Remove "Remove from board" button from backlog and closed list +merge_request: 12430 +author: diff --git a/changelogs/unreleased/34116-milestone-filtering-on-group-issues.yml b/changelogs/unreleased/34116-milestone-filtering-on-group-issues.yml new file mode 100644 index 00000000000..8f8b5a96c2b --- /dev/null +++ b/changelogs/unreleased/34116-milestone-filtering-on-group-issues.yml @@ -0,0 +1,4 @@ +--- +title: Change milestone endpoint for groups +merge_request: 12374 +author: Takuya Noguchi diff --git a/changelogs/unreleased/34141-allow-unauthenticated-access-to-the-users-api.yml b/changelogs/unreleased/34141-allow-unauthenticated-access-to-the-users-api.yml new file mode 100644 index 00000000000..a3ade8db214 --- /dev/null +++ b/changelogs/unreleased/34141-allow-unauthenticated-access-to-the-users-api.yml @@ -0,0 +1,4 @@ +--- +title: Allow unauthenticated access to the /api/v4/users API +merge_request: 12445 +author: diff --git a/changelogs/unreleased/34531-remove-scroll.yml b/changelogs/unreleased/34531-remove-scroll.yml new file mode 100644 index 00000000000..c3c5289f66f --- /dev/null +++ b/changelogs/unreleased/34531-remove-scroll.yml @@ -0,0 +1,4 @@ +--- +title: Update jobs page output to have a scrollable page +merge_request: 12587 +author: diff --git a/changelogs/unreleased/34544-add-italian-translation-of-cycle-analytics-page-&-project-page-&-repository-page.yml b/changelogs/unreleased/34544-add-italian-translation-of-cycle-analytics-page-&-project-page-&-repository-page.yml new file mode 100644 index 00000000000..31f4262c9f9 --- /dev/null +++ b/changelogs/unreleased/34544-add-italian-translation-of-cycle-analytics-page-&-project-page-&-repository-page.yml @@ -0,0 +1,4 @@ +--- +title: Add Italian translation of Cycle Analytics Page & Project Page & Repository Page +merge_request: 12578 +author: Huang Tao diff --git a/changelogs/unreleased/add-ci_variables-environment_scope-mysql.yml b/changelogs/unreleased/add-ci_variables-environment_scope-mysql.yml new file mode 100644 index 00000000000..4948d415bed --- /dev/null +++ b/changelogs/unreleased/add-ci_variables-environment_scope-mysql.yml @@ -0,0 +1,6 @@ +--- +title: Rename duplicated variables with the same key for projects. Add environment_scope + column to variables and add unique constraint to make sure that no variables could + be created with the same key within a project +merge_request: 12363 +author: diff --git a/changelogs/unreleased/dm-always-verify-source-branch-can-be-deleted.yml b/changelogs/unreleased/dm-always-verify-source-branch-can-be-deleted.yml new file mode 100644 index 00000000000..f2e1f412502 --- /dev/null +++ b/changelogs/unreleased/dm-always-verify-source-branch-can-be-deleted.yml @@ -0,0 +1,5 @@ +--- +title: Prevent accidental deletion of protected MR source branch by repeating checks + before actual deletion +merge_request: +author: diff --git a/changelogs/unreleased/dm-drop-default-scope-on-sortable-finders.yml b/changelogs/unreleased/dm-drop-default-scope-on-sortable-finders.yml new file mode 100644 index 00000000000..b359a25053a --- /dev/null +++ b/changelogs/unreleased/dm-drop-default-scope-on-sortable-finders.yml @@ -0,0 +1,4 @@ +--- +title: Improve performance of lookups of issues, merge requests etc by dropping unnecessary ORDER BY clause +merge_request: +author: diff --git a/changelogs/unreleased/dm-encode-tree-and-blob-paths.yml b/changelogs/unreleased/dm-encode-tree-and-blob-paths.yml new file mode 100644 index 00000000000..c1a026e1f29 --- /dev/null +++ b/changelogs/unreleased/dm-encode-tree-and-blob-paths.yml @@ -0,0 +1,5 @@ +--- +title: Fix issues with non-UTF8 filenames by always fixing the encoding of tree and + blob paths +merge_request: +author: diff --git a/changelogs/unreleased/feature-no-hypen-at-end-of-commit-ref-slug.yml b/changelogs/unreleased/feature-no-hypen-at-end-of-commit-ref-slug.yml new file mode 100644 index 00000000000..bbcf2946ea7 --- /dev/null +++ b/changelogs/unreleased/feature-no-hypen-at-end-of-commit-ref-slug.yml @@ -0,0 +1,4 @@ +--- +title: Omit trailing / leading hyphens in CI_COMMIT_REF_SLUG variable to make it usable as a hostname +merge_request: 11218 +author: Stefan Hanreich diff --git a/changelogs/unreleased/fix-2801.yml b/changelogs/unreleased/fix-2801.yml new file mode 100644 index 00000000000..4f0aa06b6ba --- /dev/null +++ b/changelogs/unreleased/fix-2801.yml @@ -0,0 +1,4 @@ +--- +title: Expires full_path cache after a repository is renamed/transferred +merge_request: +author: diff --git a/changelogs/unreleased/hb-hide-archived-labels-from-group-issue-tracker.yml b/changelogs/unreleased/hb-hide-archived-labels-from-group-issue-tracker.yml new file mode 100644 index 00000000000..3b465d84126 --- /dev/null +++ b/changelogs/unreleased/hb-hide-archived-labels-from-group-issue-tracker.yml @@ -0,0 +1,4 @@ +--- +title: Hide archived project labels from group issue tracker +merge_request: 12547 +author: Horacio Bertorello diff --git a/changelogs/unreleased/monitoring-dashboard-fine-tuning-ux.yml b/changelogs/unreleased/monitoring-dashboard-fine-tuning-ux.yml new file mode 100644 index 00000000000..f84d41b7929 --- /dev/null +++ b/changelogs/unreleased/monitoring-dashboard-fine-tuning-ux.yml @@ -0,0 +1,4 @@ +--- +title: Improve the overall UX for the new monitoring dashboard +merge_request: +author: diff --git a/changelogs/unreleased/monitoring-dashboard-fix-y-label.yml b/changelogs/unreleased/monitoring-dashboard-fix-y-label.yml new file mode 100644 index 00000000000..8a0e9ca855c --- /dev/null +++ b/changelogs/unreleased/monitoring-dashboard-fix-y-label.yml @@ -0,0 +1,4 @@ +--- +title: Fixed the y_label not setting correctly for each graph on the monitoring dashboard +merge_request: +author: diff --git a/changelogs/unreleased/sh-allow-force-repo-create.yml b/changelogs/unreleased/sh-allow-force-repo-create.yml new file mode 100644 index 00000000000..2a65ba807bb --- /dev/null +++ b/changelogs/unreleased/sh-allow-force-repo-create.yml @@ -0,0 +1,4 @@ +--- +title: Make Project#ensure_repository force create a repo +merge_request: +author: diff --git a/changelogs/unreleased/sh-log-application-controller-exceptions-sentry.yml b/changelogs/unreleased/sh-log-application-controller-exceptions-sentry.yml new file mode 100644 index 00000000000..ec9ceab3d81 --- /dev/null +++ b/changelogs/unreleased/sh-log-application-controller-exceptions-sentry.yml @@ -0,0 +1,4 @@ +--- +title: Log rescued exceptions to Sentry +merge_request: +author: diff --git a/changelogs/unreleased/sh-optimize-project-commit-api.yml b/changelogs/unreleased/sh-optimize-project-commit-api.yml new file mode 100644 index 00000000000..e6a8a80593c --- /dev/null +++ b/changelogs/unreleased/sh-optimize-project-commit-api.yml @@ -0,0 +1,4 @@ +--- +title: Optimize creation of commit API by using Repository#commit instead of Repository#commits +merge_request: +author: diff --git a/changelogs/unreleased/tc-no-delete-protected-merged-branches.yml b/changelogs/unreleased/tc-no-delete-protected-merged-branches.yml new file mode 100644 index 00000000000..663a0349bac --- /dev/null +++ b/changelogs/unreleased/tc-no-delete-protected-merged-branches.yml @@ -0,0 +1,4 @@ +--- +title: Do not delete protected branches when deleting all merged branches +merge_request: 12624 +author: diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 43a8c0078ca..fdc2b24e110 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -543,6 +543,10 @@ production: &base # enabled: true # host: localhost # port: 3808 + prometheus: + # Time between sampling of unicorn socket metrics, in seconds + # unicorn_sampler_interval: 10 + # # 5. Extra customization @@ -615,6 +619,53 @@ test: title: "JIRA" url: https://sample_company.atlassian.net project_key: PROJECT + + omniauth: + enabled: true + allow_single_sign_on: true + external_providers: [] + + providers: + - { name: 'cas3', + label: 'cas3', + args: { url: 'https://sso.example.com', + disable_ssl_verification: false, + login_url: '/cas/login', + service_validate_url: '/cas/p3/serviceValidate', + logout_url: '/cas/logout'} } + - { name: 'authentiq', + app_id: 'YOUR_CLIENT_ID', + app_secret: 'YOUR_CLIENT_SECRET', + args: { scope: 'aq:name email~rs address aq:push' } } + - { name: 'github', + app_id: 'YOUR_APP_ID', + app_secret: 'YOUR_APP_SECRET', + url: "https://github.com/", + verify_ssl: false, + args: { scope: 'user:email' } } + - { name: 'bitbucket', + app_id: 'YOUR_APP_ID', + app_secret: 'YOUR_APP_SECRET' } + - { name: 'gitlab', + app_id: 'YOUR_APP_ID', + app_secret: 'YOUR_APP_SECRET', + args: { scope: 'api' } } + - { name: 'google_oauth2', + app_id: 'YOUR_APP_ID', + app_secret: 'YOUR_APP_SECRET', + args: { access_type: 'offline', approval_prompt: '' } } + - { name: 'facebook', + app_id: 'YOUR_APP_ID', + app_secret: 'YOUR_APP_SECRET' } + - { name: 'twitter', + app_id: 'YOUR_APP_ID', + app_secret: 'YOUR_APP_SECRET' } + - { name: 'auth0', + args: { + client_id: 'YOUR_AUTH0_CLIENT_ID', + client_secret: 'YOUR_AUTH0_CLIENT_SECRET', + namespace: 'YOUR_AUTH0_DOMAIN' } } + ldap: enabled: false servers: diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 8ddf8e4d2e4..cb11d2c34f4 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -495,6 +495,12 @@ Settings.webpack.dev_server['host'] ||= 'localhost' Settings.webpack.dev_server['port'] ||= 3808 # +# Prometheus metrics settings +# +Settings['prometheus'] ||= Settingslogic.new({}) +Settings.prometheus['unicorn_sampler_interval'] ||= 10 + +# # Testing settings # if Rails.env.test? diff --git a/config/initializers/8_metrics.rb b/config/initializers/8_metrics.rb index a0a63ddf8f0..d56fd7a6cfa 100644 --- a/config/initializers/8_metrics.rb +++ b/config/initializers/8_metrics.rb @@ -119,6 +119,13 @@ def instrument_classes(instrumentation) end # rubocop:enable Metrics/AbcSize +Gitlab::Metrics::UnicornSampler.initialize_instance(Settings.prometheus.unicorn_sampler_interval).start + +Gitlab::Application.configure do |config| + # 0 should be Sentry to catch errors in this middleware + config.middleware.insert(1, Gitlab::Metrics::ConnectionRackMiddleware) +end + if Gitlab::Metrics.enabled? require 'pathname' require 'influxdb' @@ -175,7 +182,7 @@ if Gitlab::Metrics.enabled? GC::Profiler.enable - Gitlab::Metrics::Sampler.new.start + Gitlab::Metrics::InfluxSampler.initialize_instance.start module TrackNewRedisConnections def connect(*args) diff --git a/db/migrate/20170622135451_rename_duplicated_variable_key.rb b/db/migrate/20170622135451_rename_duplicated_variable_key.rb new file mode 100644 index 00000000000..368718ab0ce --- /dev/null +++ b/db/migrate/20170622135451_rename_duplicated_variable_key.rb @@ -0,0 +1,38 @@ +class RenameDuplicatedVariableKey < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + execute(<<~SQL) + UPDATE ci_variables + SET #{key} = CONCAT(#{key}, #{underscore}, id) + WHERE id IN ( + SELECT * + FROM ( -- MySQL requires an extra layer + SELECT dup.id + FROM ci_variables dup + INNER JOIN (SELECT max(id) AS id, #{key}, project_id + FROM ci_variables tmp + GROUP BY #{key}, project_id) var + USING (#{key}, project_id) where dup.id <> var.id + ) dummy + ) + SQL + end + + def down + # noop + end + + def key + # key needs to be quoted in MySQL + quote_column_name('key') + end + + def underscore + quote('_') + end +end diff --git a/db/migrate/20170622135628_add_environment_scope_to_ci_variables.rb b/db/migrate/20170622135628_add_environment_scope_to_ci_variables.rb new file mode 100644 index 00000000000..17fe062d8d5 --- /dev/null +++ b/db/migrate/20170622135628_add_environment_scope_to_ci_variables.rb @@ -0,0 +1,15 @@ +class AddEnvironmentScopeToCiVariables < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_column_with_default(:ci_variables, :environment_scope, :string, default: '*') + end + + def down + remove_column(:ci_variables, :environment_scope) + end +end diff --git a/db/migrate/20170622135728_add_unique_constraint_to_ci_variables.rb b/db/migrate/20170622135728_add_unique_constraint_to_ci_variables.rb new file mode 100644 index 00000000000..8b2cc40ee59 --- /dev/null +++ b/db/migrate/20170622135728_add_unique_constraint_to_ci_variables.rb @@ -0,0 +1,38 @@ +class AddUniqueConstraintToCiVariables < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + unless this_index_exists? + add_concurrent_index(:ci_variables, columns, name: index_name, unique: true) + end + end + + def down + if this_index_exists? + if Gitlab::Database.mysql? && !index_exists?(:ci_variables, :project_id) + # Need to add this index for MySQL project_id foreign key constraint + add_concurrent_index(:ci_variables, :project_id) + end + + remove_concurrent_index(:ci_variables, columns, name: index_name) + end + end + + private + + def this_index_exists? + index_exists?(:ci_variables, columns, name: index_name) + end + + def columns + @columns ||= [:project_id, :key, :environment_scope] + end + + def index_name + 'index_ci_variables_on_project_id_and_key_and_environment_scope' + end +end diff --git a/db/migrate/20170623080805_remove_ci_variables_project_id_index.rb b/db/migrate/20170623080805_remove_ci_variables_project_id_index.rb new file mode 100644 index 00000000000..ddcc0292b9d --- /dev/null +++ b/db/migrate/20170623080805_remove_ci_variables_project_id_index.rb @@ -0,0 +1,19 @@ +class RemoveCiVariablesProjectIdIndex < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + if index_exists?(:ci_variables, :project_id) + remove_concurrent_index(:ci_variables, :project_id) + end + end + + def down + unless index_exists?(:ci_variables, :project_id) + add_concurrent_index(:ci_variables, :project_id) + end + end +end diff --git a/db/migrate/20170703102400_add_stage_id_foreign_key_to_builds.rb b/db/migrate/20170703102400_add_stage_id_foreign_key_to_builds.rb new file mode 100644 index 00000000000..68b947583d3 --- /dev/null +++ b/db/migrate/20170703102400_add_stage_id_foreign_key_to_builds.rb @@ -0,0 +1,35 @@ +class AddStageIdForeignKeyToBuilds < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + unless index_exists?(:ci_builds, :stage_id) + add_concurrent_index(:ci_builds, :stage_id) + end + + unless foreign_key_exists?(:ci_builds, :stage_id) + add_concurrent_foreign_key(:ci_builds, :ci_stages, column: :stage_id, on_delete: :cascade) + end + end + + def down + if foreign_key_exists?(:ci_builds, :stage_id) + remove_foreign_key(:ci_builds, column: :stage_id) + end + + if index_exists?(:ci_builds, :stage_id) + remove_concurrent_index(:ci_builds, :stage_id) + end + end + + private + + def foreign_key_exists?(table, column) + foreign_keys(:ci_builds).any? do |key| + key.options[:column] == column.to_s + end + end +end diff --git a/db/post_migrate/20170621102400_add_stage_id_index_to_builds.rb b/db/post_migrate/20170621102400_add_stage_id_index_to_builds.rb index 7d6609b18bf..ac61b5c84a8 100644 --- a/db/post_migrate/20170621102400_add_stage_id_index_to_builds.rb +++ b/db/post_migrate/20170621102400_add_stage_id_index_to_builds.rb @@ -3,19 +3,15 @@ class AddStageIdIndexToBuilds < ActiveRecord::Migration DOWNTIME = false - disable_ddl_transaction! + ## + # Improved in 20170703102400_add_stage_id_foreign_key_to_builds.rb + # def up - unless index_exists?(:ci_builds, :stage_id) - add_concurrent_foreign_key(:ci_builds, :ci_stages, column: :stage_id, on_delete: :cascade) - add_concurrent_index(:ci_builds, :stage_id) - end + # noop end def down - if index_exists?(:ci_builds, :stage_id) - remove_foreign_key(:ci_builds, column: :stage_id) - remove_concurrent_index(:ci_builds, :stage_id) - end + # noop end end diff --git a/db/schema.rb b/db/schema.rb index 71555b21241..40f30a10a01 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: 20170622162730) do +ActiveRecord::Schema.define(version: 20170703102400) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -374,9 +374,10 @@ ActiveRecord::Schema.define(version: 20170622162730) do t.string "encrypted_value_iv" t.integer "project_id", null: false t.boolean "protected", default: false, null: false + t.string "environment_scope", default: "*", null: false end - add_index "ci_variables", ["project_id"], name: "index_ci_variables_on_project_id", using: :btree + add_index "ci_variables", ["project_id", "key", "environment_scope"], name: "index_ci_variables_on_project_id_and_key_and_environment_scope", unique: true, using: :btree create_table "container_repositories", force: :cascade do |t| t.integer "project_id", null: false diff --git a/doc/api/repository_files.md b/doc/api/repository_files.md index 18ceb8f779e..1fc577561a0 100644 --- a/doc/api/repository_files.md +++ b/doc/api/repository_files.md @@ -61,7 +61,7 @@ POST /projects/:id/repository/files/:file_path ``` ```bash -curl --request POST --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v4/projects/13083/repository/app%2Fprojectrb%2E?branch=master&author_email=author%40example.com&author_name=Firstname%20Lastname&content=some%20content&commit_message=create%20a%20new%20file' +curl --request POST --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v4/projects/13083/repository/files/app%2Fprojectrb%2E?branch=master&author_email=author%40example.com&author_name=Firstname%20Lastname&content=some%20content&commit_message=create%20a%20new%20file' ``` Example response: @@ -90,7 +90,7 @@ PUT /projects/:id/repository/files/:file_path ``` ```bash -curl --request PUT --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v4/projects/13083/repository/app%2Fproject%2Erb?branch=master&author_email=author%40example.com&author_name=Firstname%20Lastname&content=some%20other%20content&commit_message=update%20file' +curl --request PUT --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v4/projects/13083/repository/files/app%2Fproject%2Erb?branch=master&author_email=author%40example.com&author_name=Firstname%20Lastname&content=some%20other%20content&commit_message=update%20file' ``` Example response: @@ -129,7 +129,7 @@ DELETE /projects/:id/repository/files/:file_path ``` ```bash -curl --request DELETE --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v4/projects/13083/repository/app%2Fproject%2Erb?branch=master&author_email=author%40example.com&author_name=Firstname%20Lastname&commit_message=delete%20file' +curl --request DELETE --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v4/projects/13083/repository/files/app%2Fproject%2Erb?branch=master&author_email=author%40example.com&author_name=Firstname%20Lastname&commit_message=delete%20file' ``` Example response: diff --git a/doc/ci/docker/using_docker_images.md b/doc/ci/docker/using_docker_images.md index be4dea55c20..d3433594eb7 100644 --- a/doc/ci/docker/using_docker_images.md +++ b/doc/ci/docker/using_docker_images.md @@ -1,4 +1,4 @@ -# Using Docker Images +# Using Docker images GitLab CI in conjunction with [GitLab Runner](../runners/README.md) can use [Docker Engine](https://www.docker.com/) to test and build any application. @@ -17,14 +17,16 @@ can also run on your workstation. The added benefit is that you can test all the commands that we will explore later from your shell, rather than having to test them on a dedicated CI server. -## Register docker runner +## Register Docker Runner -To use GitLab Runner with docker you need to register a new runner to use the -`docker` executor: +To use GitLab Runner with Docker you need to [register a new Runner][register] +to use the `docker` executor. + +A one-line example can be seen below: ```bash -gitlab-ci-multi-runner register \ - --url "https://gitlab.com/" \ +sudo gitlab-runner register \ + --url "https://gitlab.example.com/" \ --registration-token "PROJECT_REGISTRATION_TOKEN" \ --description "docker-ruby-2.1" \ --executor "docker" \ @@ -33,26 +35,26 @@ gitlab-ci-multi-runner register \ --docker-mysql latest ``` -The registered runner will use the `ruby:2.1` docker image and will run two +The registered runner will use the `ruby:2.1` Docker image and will run two services, `postgres:latest` and `mysql:latest`, both of which will be accessible during the build process. ## What is an image -The `image` keyword is the name of the docker image the docker executor -will run to perform the CI tasks. +The `image` keyword is the name of the Docker image the Docker executor +will run to perform the CI tasks. -By default the executor will only pull images from [Docker Hub][hub], +By default, the executor will only pull images from [Docker Hub][hub], but this can be configured in the `gitlab-runner/config.toml` by setting -the [docker pull policy][] to allow using local images. +the [Docker pull policy][] to allow using local images. For more information about images and Docker Hub please read the [Docker Fundamentals][] documentation. ## What is a service -The `services` keyword defines just another docker image that is run during -your job and is linked to the docker image that the `image` keyword defines. +The `services` keyword defines just another Docker image that is run during +your job and is linked to the Docker image that the `image` keyword defines. This allows you to access the service image during build time. The service image can run any application, but the most common use case is to @@ -60,6 +62,11 @@ run a database container, eg. `mysql`. It's easier and faster to use an existing image and run it as an additional container than install `mysql` every time the project is built. +You are not limited to have only database services. You can add as many +services you need to `.gitlab-ci.yml` or manually modify `config.toml`. +Any image found at [Docker Hub][hub] or your private Container Registry can be +used as a service. + You can see some widely used services examples in the relevant documentation of [CI services examples](../services/README.md). @@ -73,22 +80,49 @@ then be used to create a container that is linked to the job container. The service container for MySQL will be accessible under the hostname `mysql`. So, in order to access your database service you have to connect to the host -named `mysql` instead of a socket or `localhost`. +named `mysql` instead of a socket or `localhost`. Read more in [accessing the +services](#accessing-the-services). -## Overwrite image and services +### Accessing the services -See [How to use other images as services](#how-to-use-other-images-as-services). +Let's say that you need a Wordpress instance to test some API integration with +your application. -## How to use other images as services +You can then use for example the [tutum/wordpress][] image in your +`.gitlab-ci.yml`: -You are not limited to have only database services. You can add as many -services you need to `.gitlab-ci.yml` or manually modify `config.toml`. -Any image found at [Docker Hub][hub] can be used as a service. +```yaml +services: +- tutum/wordpress:latest +``` + +If you don't [specify a service alias](#available-settings-for-services-entry), +when the job is run, `tutum/wordpress` will be started and you will have +access to it from your build container under two hostnames to choose from: -## Define image and services from `.gitlab-ci.yml` +- `tutum-wordpress` +- `tutum__wordpress` + +>**Note:** +Hostnames with underscores are not RFC valid and may cause problems in 3rd party +applications. + +The default aliases for the service's hostname are created from its image name +following these rules: + +- Everything after the colon (`:`) is stripped +- Slash (`/`) is replaced with double underscores (`__`) and the primary alias + is created +- Slash (`/`) is replaced with a single dash (`-`) and the secondary alias is + created (requires GitLab Runner v1.1.0 or higher) + +To override the default behavior, you can +[specify a service alias](#available-settings-for-services-entry). + +## Define `image` and `services` from `.gitlab-ci.yml` You can simply define an image that will be used for all jobs and a list of -services that you want to use during build time. +services that you want to use during build time: ```yaml image: ruby:2.2 @@ -125,6 +159,203 @@ test:2.2: - bundle exec rake spec ``` +Or you can pass some [extended configuration options](#extended-docker-configuration-options) +for `image` and `services`: + +```yaml +image: + name: ruby:2.2 + entrypoint: ["/bin/bash"] + +services: +- name: my-postgres:9.4 + alias: db-postgres + entrypoint: ["/usr/local/bin/db-postgres"] + command: ["start"] + +before_script: +- bundle install + +test: + script: + - bundle exec rake spec +``` + +## Extended Docker configuration options + +> **Note:** +This feature requires GitLab 9.4 and GitLab Runner 9.4 or higher. + +When configuring the `image` or `services` entries, you can use a string or a map as +options: + +- when using a string as an option, it must be the full name of the image to use + (including the Registry part if you want to download the image from a Registry + other than Docker Hub) +- when using a map as an option, then it must contain at least the `name` + option, which is the same name of the image as used for the string setting + +For example, the following two definitions are equal: + +1. Using a string as an option to `image` and `services`: + + ```yaml + image: "registry.example.com/my/image:latest" + + services: + - postgresql:9.4 + - redis:latest + ``` + +1. Using a map as an option to `image` and `services`. The use of `image:name` is + required: + + ```yaml + image: + name: "registry.example.com/my/image:latest" + + services: + - name: postgresql:9.4 + - name: redis:latest + ``` + +### Available settings for `image` + +> **Note:** +This feature requires GitLab 9.4 and GitLab Runner 9.4 or higher. + +| Setting | Required | Description | +|------------|----------|-------------| +| `name` | yes, when used with any other option | Full name of the image that should be used. It should contain the Registry part if needed. | +| `entrypoint` | no | Command or script that should be executed as the container's entrypoint. It will be translated to Docker's `--entrypoint` option while creating the container. The syntax is similar to [`Dockerfile`'s `ENTRYPOINT`][entrypoint] directive, where each shell token is a separate string in the array. | + +### Available settings for `services` + +> **Note:** +This feature requires GitLab 9.4 and GitLab Runner 9.4 or higher. + +| Setting | Required | Description | +|------------|----------|-------------| +| `name` | yes, when used with any other option | Full name of the image that should be used. It should contain the Registry part if needed. | +| `entrypoint` | no | Command or script that should be executed as the container's entrypoint. It will be translated to Docker's `--entrypoint` option while creating the container. The syntax is similar to [`Dockerfile`'s `ENTRYPOINT`][entrypoint] directive, where each shell token is a separate string in the array. | +| `command` | no | Command or script that should be used as the container's command. It will be translated to arguments passed to Docker after the image's name. The syntax is similar to [`Dockerfile`'s `CMD`][cmd] directive, where each shell token is a separate string in the array. | +| `alias` | no | Additional alias that can be used to access the service from the job's container. Read [Accessing the services](#accessing-the-services) for more information. | + +### Starting multiple services from the same image + +Before the new extended Docker configuration options, the following configuration +would not work properly: + +```yaml +services: +- mysql:latest +- mysql:latest +``` + +The Runner would start two containers using the `mysql:latest` image, but both +of them would be added to the job's container with the `mysql` alias based on +the [default hostname naming](#accessing-the-services). This would end with one +of the services not being accessible. + +After the new extended Docker configuration options, the above example would +look like: + +```yaml +services: +- name: mysql:latest + alias: mysql-1 +- name: mysql:latest + alias: mysql-2 +``` + +The Runner will still start two containers using the `mysql:latest` image, +but now each of them will also be accessible with the alias configured +in `.gitlab-ci.yml` file. + +### Setting a command for the service + +Let's assume you have a `super/sql:latest` image with some SQL database +inside it and you would like to use it as a service for your job. Let's also +assume that this image doesn't start the database process while starting +the container and the user needs to manually use `/usr/bin/super-sql run` as +a command to start the database. + +Before the new extended Docker configuration options, you would need to create +your own image based on the `super/sql:latest` image, add the default command, +and then use it in job's configuration, like: + +```Dockerfile +# my-super-sql:latest image's Dockerfile + +FROM super/sql:latest +CMD ["/usr/bin/super-sql", "run"] +``` + +```yaml +# .gitlab-ci.yml + +services: +- my-super-sql:latest +``` + +After the new extended Docker configuration options, you can now simply +set a `command` in `.gitlab-ci.yml`, like: + +```yaml +# .gitlab-ci.yml + +services: +- name: super/sql:latest + command: ["/usr/bin/super-sql", "run"] +``` + +As you can see, the syntax of `command` is similar to [Dockerfile's `CMD`][cmd]. + +### Overriding the entrypoint of an image + +Let's assume you have a `super/sql:experimental` image with some SQL database +inside it and you would like to use it as a base image for your job because you +want to execute some tests with this database binary. Let's also assume that +this image is configured with `/usr/bin/super-sql run` as an entrypoint. That +means, that when starting the container without additional options, it will run +the database's process, while Runner expects that the image will have no +entrypoint or at least will start with a shell as its entrypoint. + +Previously we would need to create our own image based on the +`super/sql:experimental` image, set the entrypoint to a shell, and then use +it in job's configuration, e.g.: + +Before the new extended Docker configuration options, you would need to create +your own image based on the `super/sql:experimental` image, set the entrypoint +to a shell and then use it in job's configuration, like: + +```Dockerfile +# my-super-sql:experimental image's Dockerfile + +FROM super/sql:experimental +ENTRYPOINT ["/bin/sh"] +``` + +```yaml +# .gitlab-ci.yml + +image: my-super-sql:experimental +``` + +After the new extended Docker configuration options, you can now simply +set an `entrypoint` in `.gitlab-ci.yml`, like: + +```yaml +# .gitlab-ci.yml + +image: + name: super/sql:experimental + entrypoint: ["/bin/sh"] +``` + +As you can see the syntax of `entrypoint` is similar to +[Dockerfile's `ENTRYPOINT`][entrypoint]. + ## Define image and services in `config.toml` Look for the `[runners.docker]` section: @@ -138,7 +369,7 @@ Look for the `[runners.docker]` section: The image and services defined this way will be added to all job run by that runner. -## Define an image from a private Docker registry +## Define an image from a private Container Registry > **Notes:** - This feature requires GitLab Runner **1.8** or higher @@ -193,44 +424,6 @@ To configure access for `registry.example.com`, follow these steps: You can add configuration for as many registries as you want, adding more registries to the `"auths"` hash as described above. -## Accessing the services - -Let's say that you need a Wordpress instance to test some API integration with -your application. - -You can then use for example the [tutum/wordpress][] image in your -`.gitlab-ci.yml`: - -```yaml -services: -- tutum/wordpress:latest -``` - -When the job is run, `tutum/wordpress` will be started and you will have -access to it from your build container under the hostnames `tutum-wordpress` -(requires GitLab Runner v1.1.0 or newer) and `tutum__wordpress`. - -When using a private registry, the image name also includes a hostname and port -of the registry. - -```yaml -services: -- docker.example.com:5000/wordpress:latest -``` - -The service hostname will also include the registry hostname. Service will be -available under hostnames `docker.example.com-wordpress` (requires GitLab Runner v1.1.0 or newer) -and `docker.example.com__wordpress`. - -*Note: hostname with underscores is not RFC valid and may cause problems in 3rd party applications.* - -The alias hostnames for the service are made from the image name following these -rules: - -1. Everything after `:` is stripped -2. Slash (`/`) is replaced with double underscores (`__`) - primary alias -3. Slash (`/`) is replaced with dash (`-`) - secondary alias, requires GitLab Runner v1.1.0 or newer - ## Configuring services Many services accept environment variables which allow you to easily change @@ -257,7 +450,7 @@ See the specific documentation for ## How Docker integration works -Below is a high level overview of the steps performed by docker during job +Below is a high level overview of the steps performed by Docker during job time. 1. Create any service container: `mysql`, `postgresql`, `mongodb`, `redis`. @@ -274,7 +467,7 @@ time. ## How to debug a job locally *Note: The following commands are run without root privileges. You should be -able to run docker with your regular user account.* +able to run Docker with your regular user account.* First start with creating a file named `build_script`: @@ -334,3 +527,6 @@ creation. [mysql-hub]: https://hub.docker.com/r/_/mysql/ [runner-priv-reg]: http://docs.gitlab.com/runner/configuration/advanced-configuration.html#using-a-private-container-registry [secret variable]: ../variables/README.md#secret-variables +[entrypoint]: https://docs.docker.com/engine/reference/builder/#entrypoint +[cmd]: https://docs.docker.com/engine/reference/builder/#cmd +[register]: https://docs.gitlab.com/runner/register/ diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md index 06bb90fa2e8..ee23ac0adbe 100644 --- a/doc/ci/variables/README.md +++ b/doc/ci/variables/README.md @@ -37,7 +37,7 @@ future GitLab releases.** |-------------------------------- |--------|--------|-------------| | **CI** | all | 0.4 | Mark that job is executed in CI environment | | **CI_COMMIT_REF_NAME** | 9.0 | all | The branch or tag name for which project is built | -| **CI_COMMIT_REF_SLUG** | 9.0 | all | `$CI_COMMIT_REF_NAME` lowercased, shortened to 63 bytes, and with everything except `0-9` and `a-z` replaced with `-`. Use in URLs and domain names. | +| **CI_COMMIT_REF_SLUG** | 9.0 | all | `$CI_COMMIT_REF_NAME` lowercased, shortened to 63 bytes, and with everything except `0-9` and `a-z` replaced with `-`. No leading / trailing `-`. Use in URLs, host names and domain names. | | **CI_COMMIT_SHA** | 9.0 | all | The commit revision for which project is built | | **CI_COMMIT_TAG** | 9.0 | 0.5 | The commit tag name. Present only when building tags. | | **CI_CONFIG_PATH** | 9.4 | 0.5 | The path to CI config file. Defaults to `.gitlab-ci.yml` | diff --git a/doc/install/database_mysql.md b/doc/install/database_mysql.md index 9a171d34671..37e9b3101ca 100644 --- a/doc/install/database_mysql.md +++ b/doc/install/database_mysql.md @@ -43,7 +43,7 @@ mysql> SET GLOBAL innodb_file_per_table=1, innodb_file_format=Barracuda, innodb_ mysql> CREATE DATABASE IF NOT EXISTS `gitlabhq_production` DEFAULT CHARACTER SET `utf8` COLLATE `utf8_general_ci`; # Grant the GitLab user necessary permissions on the database -mysql> GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, CREATE TEMPORARY TABLES, DROP, INDEX, ALTER, LOCK TABLES, REFERENCES ON `gitlabhq_production`.* TO 'git'@'localhost'; +mysql> GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, CREATE TEMPORARY TABLES, DROP, INDEX, ALTER, LOCK TABLES, REFERENCES, TRIGGER ON `gitlabhq_production`.* TO 'git'@'localhost'; # Quit the database session mysql> \q diff --git a/doc/update/8.9-to-8.10.md b/doc/update/8.9-to-8.10.md index d6b2f11d49a..42132f690d8 100644 --- a/doc/update/8.9-to-8.10.md +++ b/doc/update/8.9-to-8.10.md @@ -156,7 +156,7 @@ See [smtp_settings.rb.sample] as an example. Ensure you're still up-to-date with the latest init script changes: sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab - + For Ubuntu 16.04.1 LTS: sudo systemctl daemon-reload diff --git a/doc/update/9.1-to-9.2.md b/doc/update/9.1-to-9.2.md index e7d97fde14e..225a4dcc924 100644 --- a/doc/update/9.1-to-9.2.md +++ b/doc/update/9.1-to-9.2.md @@ -70,7 +70,27 @@ curl --location https://yarnpkg.com/install.sh | bash - More information can be found on the [yarn website](https://yarnpkg.com/en/docs/install). -### 5. Get latest code +### 5. Update Go + +NOTE: GitLab 9.2 and higher only supports Go 1.8.3 and dropped support for Go +1.5.x through 1.7.x. Be sure to upgrade your installation if necessary. + +You can check which version you are running with `go version`. + +Download and install Go: + +```bash +# Remove former Go installation folder +sudo rm -rf /usr/local/go + +curl --remote-name --progress https://storage.googleapis.com/golang/go1.8.3.linux-amd64.tar.gz +echo '1862f4c3d3907e59b04a757cfda0ea7aa9ef39274af99a784f5be843c80c6772 go1.8.3.linux-amd64.tar.gz' | shasum -a256 -c - && \ + sudo tar -C /usr/local -xzf go1.8.3.linux-amd64.tar.gz +sudo ln -sf /usr/local/go/bin/{go,godoc,gofmt} /usr/local/bin/ +rm go1.8.3.linux-amd64.tar.gz +``` + +### 6. Get latest code ```bash cd /home/git/gitlab @@ -97,7 +117,7 @@ cd /home/git/gitlab sudo -u git -H git checkout 9-2-stable-ee ``` -### 6. Update gitlab-shell +### 7. Update gitlab-shell ```bash cd /home/git/gitlab-shell @@ -107,11 +127,10 @@ sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_SHELL_VERSION) sudo -u git -H bin/compile ``` -### 7. Update gitlab-workhorse +### 8. Update gitlab-workhorse -Install and compile gitlab-workhorse. This requires -[Go 1.8](https://golang.org/dl). Go (at least 1.5) should already be on your system from -GitLab 8.1 and shall be upgraded if necessary. Please note that starting in Gitlab 9.3, only Go 1.8.3 and above will be supported. GitLab-Workhorse uses [GNU Make](https://www.gnu.org/software/make/). +Install and compile gitlab-workhorse. GitLab-Workhorse uses +[GNU Make](https://www.gnu.org/software/make/). If you are not using Linux you may have to run `gmake` instead of `make` below. @@ -123,7 +142,7 @@ sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_WORKHORSE_VERSION) sudo -u git -H make ``` -### 8. Update configuration files +### 9. Update configuration files #### New configuration options for `gitlab.yml` @@ -197,7 +216,7 @@ For Ubuntu 16.04.1 LTS: sudo systemctl daemon-reload ``` -### 9. Install libs, migrations, etc. +### 10. Install libs, migrations, etc. ```bash cd /home/git/gitlab @@ -223,7 +242,7 @@ sudo -u git -H bundle exec rake cache:clear RAILS_ENV=production **MySQL installations**: Run through the `MySQL strings limits` and `Tables and data conversion to utf8mb4` [tasks](../install/database_mysql.md). -### 10. Optional: install Gitaly +### 11. Optional: install Gitaly Gitaly is still an optional component of GitLab. If you want to save time during your 9.2 upgrade **you can skip this step**. @@ -240,14 +259,14 @@ sudo -u git -H git checkout v$(</home/git/gitlab/GITALY_SERVER_VERSION) sudo -u git -H make ``` -### 11. Start application +### 12. Start application ```bash sudo service gitlab start sudo service nginx restart ``` -### 12. Check application status +### 13. Check application status Check if GitLab and its environment are configured correctly: diff --git a/doc/update/9.2-to-9.3.md b/doc/update/9.2-to-9.3.md index 8fbcc892fd5..097b996ec31 100644 --- a/doc/update/9.2-to-9.3.md +++ b/doc/update/9.2-to-9.3.md @@ -72,8 +72,8 @@ More information can be found on the [yarn website](https://yarnpkg.com/en/docs/ ### 5. Update Go -NOTE: GitLab 9.3 and higher only supports Go 1.8.3 and dropped support for Go 1.5.x through 1.7.x. Be -sure to upgrade your installation if necessary +NOTE: GitLab 9.2 and higher only supports Go 1.8.3 and dropped support for Go +1.5.x through 1.7.x. Be sure to upgrade your installation if necessary. You can check which version you are running with `go version`. @@ -129,9 +129,8 @@ sudo -u git -H bin/compile ### 8. Update gitlab-workhorse -Install and compile gitlab-workhorse. This requires -[Go 1.5](https://golang.org/dl) which should already be on your system from -GitLab 8.1. GitLab-Workhorse uses [GNU Make](https://www.gnu.org/software/make/). +Install and compile gitlab-workhorse. GitLab-Workhorse uses +[GNU Make](https://www.gnu.org/software/make/). If you are not using Linux you may have to run `gmake` instead of `make` below. @@ -157,7 +156,16 @@ sudo -u git -H git checkout v$(</home/git/gitlab/GITALY_SERVER_VERSION) sudo -u git -H make ``` -### 10. Update configuration files +### 10. Update MySQL permissions + +If you are using MySQL you need to grant the GitLab user the necessary +permissions on the database: + +```bash +mysql -u root -p -e "GRANT TRIGGER ON \`gitlabhq_production\`.* TO 'git'@'localhost';" +``` + +### 11. Update configuration files #### New configuration options for `gitlab.yml` @@ -231,7 +239,7 @@ For Ubuntu 16.04.1 LTS: sudo systemctl daemon-reload ``` -### 11. Install libs, migrations, etc. +### 12. Install libs, migrations, etc. ```bash cd /home/git/gitlab @@ -257,14 +265,14 @@ sudo -u git -H bundle exec rake cache:clear RAILS_ENV=production **MySQL installations**: Run through the `MySQL strings limits` and `Tables and data conversion to utf8mb4` [tasks](../install/database_mysql.md). -### 12. Start application +### 13. Start application ```bash sudo service gitlab start sudo service nginx restart ``` -### 13. Check application status +### 14. Check application status Check if GitLab and its environment are configured correctly: diff --git a/doc/user/project/img/issue_board_move_issue_card_list.png b/doc/user/project/img/issue_board_move_issue_card_list.png Binary files differnew file mode 100644 index 00000000000..c6b17ada40e --- /dev/null +++ b/doc/user/project/img/issue_board_move_issue_card_list.png diff --git a/doc/user/project/issue_board.md b/doc/user/project/issue_board.md index ebea7062ecb..e2cc67726e0 100644 --- a/doc/user/project/issue_board.md +++ b/doc/user/project/issue_board.md @@ -1,8 +1,7 @@ -# Issue board +# Issue Board ->**Notes:** -- [Introduced][ce-5554] in GitLab 8.11. -- The Backlog column was replaced by the **Add issues** button in GitLab 8.17. +>**Note:** +[Introduced][ce-5554] in [GitLab 8.11](https://about.gitlab.com/2016/08/22/gitlab-8-11-released/#issue-board). The GitLab Issue Board is a software project management tool used to plan, organize, and visualize a workflow for a feature or product release. @@ -15,12 +14,65 @@ Other interesting links: ## Overview -The Issue Board builds on GitLab's existing issue tracking functionality and +The Issue Board builds on GitLab's existing +[issue tracking functionality](issues/index.md#issue-tracker) and leverages the power of [labels] by utilizing them as lists of the scrum board. -With the Issue Board you can have a different view of your issues while also +With the Issue Board you can have a different view of your issues while maintaining the same filtering and sorting abilities you see across the -issue tracker. +issue tracker. An Issue Board is based on its project's label structure, therefore, it +applies the same descriptive labels to indicate placement on the board, keeping +consistency throughout the entire development lifecycle. + +An Issue Board shows you what issues your team is working on, who is assigned to each, +and where in the workflow those issues are. + +You create issues, host code, perform reviews, build, test, +and deploy from one single platform. Issue Boards help you to visualize +and manage the entire process _in_ GitLab. + +With [Multiple Issue Boards](https://docs.gitlab.com/ee/user/project/issue_board.html#multiple-issue-boards), available +only in [GitLab Enterprise Edition](https://about.gitlab.com/gitlab-ee/), +you go even further, as you can not only keep yourself and your project +organized from a broader perspective with one Issue Board per project, +but also allow your team members to organize their own workflow by creating +multiple Issue Boards within the same project. + +## Use cases + +GitLab Workflow allows you to discuss proposals in issues, categorize them +with labels, and from there organize and prioritize them with Issue Boards. + +For example, let's consider this simplified development workflow: + +1. You have a repository hosting your app's codebase +and your team actively contributing to code +1. Your **backend** team starts working a new +implementation, gathers feedback and approval, and pass it over to **frontend** +1. When frontend is complete, the new feature is deployed to **staging** to be tested +1. When successful, it is deployed to **production** + +If we have the labels "**backend**", "**frontend**", "**staging**", and +"**production**", and an Issue Board with a list for each, we can: + +- Visualize the entire flow of implementations since the +beginning of the development lifecycle until deployed to production +- Prioritize the issues in a list by moving them vertically +- Move issues between lists to organize them according to the labels you've set +- Add multiple issues to lists in the board by selecting one or more existing issues + +![issue card moving](img/issue_board_move_issue_card_list.png) + +> **Notes:** +> +>- For a broader use case, please check the blog post +[GitLab Workflow, an Overview](https://about.gitlab.com/2016/10/25/gitlab-workflow-an-overview/#gitlab-workflow-use-case-scenario). +> +>- For a real use case, please check why +[Codepen decided to adopt Issue Boards](https://about.gitlab.com/2017/01/27/codepen-welcome-to-gitlab/#project-management-everything-in-one-place) +to improve their workflow with [multiple boards](https://docs.gitlab.com/ee/user/project/issue_board.html#multiple-issue-boards). + +## Issue Board terminology Below is a table of the definitions used for GitLab's Issue Board. @@ -57,7 +109,7 @@ In short, here's a list of actions you can take in an Issue Board: If you are not able to perform one or more of the things above, make sure you have the right [permissions](#permissions). -## First time using the issue board +## First time using the Issue Board The first time you navigate to your Issue Board, you will be presented with a default list (**Done**) and a welcoming message that gives @@ -98,7 +150,7 @@ list view that is removed. You can always add it back later if you need. ## Adding issues to a list You can add issues to a list by clicking the **Add issues** button that is -present in the upper right corner of the issue board. This will open up a modal +present in the upper right corner of the Issue Board. This will open up a modal window where you can see all the issues that do not belong to any list. Select one or more issues by clicking on the cards and then click **Add issues** diff --git a/doc/user/project/issues/index.md b/doc/user/project/issues/index.md index fe87e6f9495..e55e2aea023 100644 --- a/doc/user/project/issues/index.md +++ b/doc/user/project/issues/index.md @@ -1,4 +1,4 @@ -# Issues documentation +# Issues The GitLab Issue Tracker is an advanced and complete tool for tracking the evolution of a new idea or the process diff --git a/doc/workflow/gitlab_flow.md b/doc/workflow/gitlab_flow.md index e10ccc4fc46..ea28968fbb2 100644 --- a/doc/workflow/gitlab_flow.md +++ b/doc/workflow/gitlab_flow.md @@ -300,7 +300,7 @@ If there are no merge conflicts and the feature branches are short lived the ris If there are merge conflicts you merge the master branch into the feature branch and the CI server will rerun the tests. If you have long lived feature branches that last for more than a few days you should make your issues smaller. -## Working wih feature branches +## Working with feature branches ![Shell output showing git pull output](git_pull.png) diff --git a/features/dashboard/new_project.feature b/features/dashboard/new_project.feature deleted file mode 100644 index 046e2815d4e..00000000000 --- a/features/dashboard/new_project.feature +++ /dev/null @@ -1,30 +0,0 @@ -@dashboard -Feature: New Project -Background: - Given I sign in as a user - And I own project "Shop" - And I visit dashboard page - And I click "New project" link - - @javascript - Scenario: I should see New Projects page - Then I see "New Project" page - Then I see all possible import options - - @javascript - Scenario: I should see instructions on how to import from Git URL - Given I see "New Project" page - When I click on "Repo by URL" - Then I see instructions on how to import from Git URL - - @javascript - Scenario: I should see instructions on how to import from GitHub - Given I see "New Project" page - When I click on "Import project from GitHub" - Then I am redirected to the GitHub import page - - @javascript - Scenario: I should see Google Code import page - Given I see "New Project" page - When I click on "Google Code" - Then I redirected to Google Code import page diff --git a/features/steps/dashboard/new_project.rb b/features/steps/dashboard/new_project.rb deleted file mode 100644 index 530fd6f7bdb..00000000000 --- a/features/steps/dashboard/new_project.rb +++ /dev/null @@ -1,59 +0,0 @@ -class Spinach::Features::NewProject < Spinach::FeatureSteps - include SharedAuthentication - include SharedPaths - include SharedProject - - step 'I click "New project" link' do - page.within '#content-body' do - click_link "New project" - end - end - - step 'I click "New project" in top right menu' do - page.within '.header-content' do - click_link "New project" - end - end - - step 'I see "New Project" page' do - expect(page).to have_content('Project path') - expect(page).to have_content('Project name') - end - - step 'I see all possible import options' do - expect(page).to have_link('GitHub') - expect(page).to have_link('Bitbucket') - expect(page).to have_link('GitLab.com') - expect(page).to have_link('Google Code') - expect(page).to have_button('Repo by URL') - expect(page).to have_link('GitLab export') - end - - step 'I click on "Import project from GitHub"' do - first('.import_github').click - end - - step 'I am redirected to the GitHub import page' do - expect(page).to have_content('Import Projects from GitHub') - expect(current_path).to eq new_import_github_path - end - - step 'I click on "Repo by URL"' do - first('.import_git').click - end - - step 'I see instructions on how to import from Git URL' do - git_import_instructions = first('.js-toggle-content') - expect(git_import_instructions).to be_visible - expect(git_import_instructions).to have_content "Git repository URL" - end - - step 'I click on "Google Code"' do - first('.import_google_code').click - end - - step 'I redirected to Google Code import page' do - expect(page).to have_content('Import projects from Google Code') - expect(current_path).to eq new_import_google_code_path - end -end diff --git a/features/steps/dashboard/starred_projects.rb b/features/steps/dashboard/starred_projects.rb deleted file mode 100644 index c33813e550b..00000000000 --- a/features/steps/dashboard/starred_projects.rb +++ /dev/null @@ -1,15 +0,0 @@ -class Spinach::Features::DashboardStarredProjects < Spinach::FeatureSteps - include SharedAuthentication - include SharedPaths - include SharedProject - - step 'I starred project "Community"' do - current_user.toggle_star(Project.find_by(name: 'Community')) - end - - step 'I should not see project "Shop"' do - page.within '.projects-list' do - expect(page).not_to have_content('Shop') - end - end -end diff --git a/lib/api/api.rb b/lib/api/api.rb index d767af36e8e..efcf0976a81 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -2,6 +2,8 @@ module API class API < Grape::API include APIGuard + allow_access_with_scope :api + version %w(v3 v4), using: :path version 'v3', using: :path do @@ -44,7 +46,6 @@ module API mount ::API::V3::Variables end - before { allow_access_with_scope :api } before { header['X-Frame-Options'] = 'SAMEORIGIN' } before { Gitlab::I18n.locale = current_user&.preferred_language } diff --git a/lib/api/api_guard.rb b/lib/api/api_guard.rb index 9fcf04efa38..0d2d71e336a 100644 --- a/lib/api/api_guard.rb +++ b/lib/api/api_guard.rb @@ -23,6 +23,23 @@ module API install_error_responders(base) end + class_methods do + # Set the authorization scope(s) allowed for an API endpoint. + # + # A call to this method maps the given scope(s) to the current API + # endpoint class. If this method is called multiple times on the same class, + # the scopes are all aggregated. + def allow_access_with_scope(scopes, options = {}) + Array(scopes).each do |scope| + allowed_scopes << Scope.new(scope, options) + end + end + + def allowed_scopes + @scopes ||= [] + end + end + # Helper Methods for Grape Endpoint module HelperMethods # Invokes the doorkeeper guard. @@ -47,7 +64,7 @@ module API access_token = find_access_token return nil unless access_token - case AccessTokenValidationService.new(access_token).validate(scopes: scopes) + case AccessTokenValidationService.new(access_token, request: request).validate(scopes: scopes) when AccessTokenValidationService::INSUFFICIENT_SCOPE raise InsufficientScopeError.new(scopes) @@ -74,18 +91,6 @@ module API @current_user end - # Set the authorization scope(s) allowed for the current request. - # - # Note: A call to this method adds to any previous scopes in place. This is done because - # `Grape` callbacks run from the outside-in: the top-level callback (API::API) runs first, then - # the next-level callback (API::API::Users, for example) runs. All these scopes are valid for the - # given endpoint (GET `/api/users` is accessible by the `api` and `read_user` scopes), and so they - # need to be stored. - def allow_access_with_scope(*scopes) - @scopes ||= [] - @scopes.concat(scopes.map(&:to_s)) - end - private def find_user_by_authentication_token(token_string) @@ -96,7 +101,7 @@ module API access_token = PersonalAccessToken.active.find_by_token(token_string) return unless access_token - if AccessTokenValidationService.new(access_token).include_any_scope?(scopes) + if AccessTokenValidationService.new(access_token, request: request).include_any_scope?(scopes) User.find(access_token.user_id) end end diff --git a/lib/api/commits.rb b/lib/api/commits.rb index c6fc17cc391..bcb842b9211 100644 --- a/lib/api/commits.rb +++ b/lib/api/commits.rb @@ -67,7 +67,7 @@ module API result = ::Files::MultiService.new(user_project, current_user, attrs).execute if result[:status] == :success - commit_detail = user_project.repository.commits(result[:result], limit: 1).first + commit_detail = user_project.repository.commit(result[:result]) present commit_detail, with: Entities::RepoCommitDetail else render_api_error!(result[:message], 400) diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index 2c73a6fdc4e..a2a661b205c 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -342,8 +342,8 @@ module API def initial_current_user return @initial_current_user if defined?(@initial_current_user) Gitlab::Auth::UniqueIpsLimiter.limit_user! do - @initial_current_user ||= find_user_by_private_token(scopes: @scopes) - @initial_current_user ||= doorkeeper_guard(scopes: @scopes) + @initial_current_user ||= find_user_by_private_token(scopes: scopes_registered_for_endpoint) + @initial_current_user ||= doorkeeper_guard(scopes: scopes_registered_for_endpoint) @initial_current_user ||= find_user_from_warden unless @initial_current_user && Gitlab::UserAccess.new(@initial_current_user).allowed? @@ -407,5 +407,22 @@ module API exception.status == 500 end + + # An array of scopes that were registered (using `allow_access_with_scope`) + # for the current endpoint class. It also returns scopes registered on + # `API::API`, since these are meant to apply to all API routes. + def scopes_registered_for_endpoint + @scopes_registered_for_endpoint ||= + begin + endpoint_classes = [options[:for].presence, ::API::API].compact + endpoint_classes.reduce([]) do |memo, endpoint| + if endpoint.respond_to?(:allowed_scopes) + memo.concat(endpoint.allowed_scopes) + else + memo + end + end + end + end end end diff --git a/lib/api/scope.rb b/lib/api/scope.rb new file mode 100644 index 00000000000..d5165b2e482 --- /dev/null +++ b/lib/api/scope.rb @@ -0,0 +1,23 @@ +# Encapsulate a scope used for authorization, such as `api`, or `read_user` +module API + class Scope + attr_reader :name, :if + + def initialize(name, options = {}) + @name = name.to_sym + @if = options[:if] + end + + # Are the `scopes` passed in sufficient to adequately authorize the passed + # request for the scope represented by the current instance of this class? + def sufficient?(scopes, request) + scopes.include?(self.name) && verify_if_condition(request) + end + + private + + def verify_if_condition(request) + self.if.nil? || self.if.call(request) + end + end +end diff --git a/lib/api/users.rb b/lib/api/users.rb index f9555842daf..88bca235692 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -1,13 +1,15 @@ module API class Users < Grape::API include PaginationParams + include APIGuard - before do - allow_access_with_scope :read_user if request.get? - authenticate! - end + allow_access_with_scope :read_user, if: -> (request) { request.get? } resource :users, requirements: { uid: /[0-9]*/, id: /[0-9]*/ } do + before do + authenticate_non_get! + end + helpers do def find_user(params) id = params[:user_id] || params[:id] @@ -51,15 +53,22 @@ module API use :pagination end get do - unless can?(current_user, :read_users_list) - render_api_error!("Not authorized.", 403) - end - authenticated_as_admin! if params[:external].present? || (params[:extern_uid].present? && params[:provider].present?) users = UsersFinder.new(current_user, params).execute - entity = current_user.admin? ? Entities::UserWithAdmin : Entities::UserBasic + authorized = can?(current_user, :read_users_list) + + # When `current_user` is not present, require that the `username` + # parameter is passed, to prevent an unauthenticated user from accessing + # a list of all the users on the GitLab instance. `UsersFinder` performs + # an exact match on the `username` parameter, so we are guaranteed to + # get either 0 or 1 `users` here. + authorized &&= params[:username].present? if current_user.blank? + + forbidden!("Not authorized to access /api/v4/users") unless authorized + + entity = current_user&.admin? ? Entities::UserWithAdmin : Entities::UserBasic present paginate(users), with: entity end @@ -398,6 +407,10 @@ module API end resource :user do + before do + authenticate! + end + desc 'Get the currently authenticated user' do success Entities::UserPublic end diff --git a/lib/api/v3/users.rb b/lib/api/v3/users.rb index 37020019e07..cf106f2552d 100644 --- a/lib/api/v3/users.rb +++ b/lib/api/v3/users.rb @@ -2,9 +2,11 @@ module API module V3 class Users < Grape::API include PaginationParams + include APIGuard + + allow_access_with_scope :read_user, if: -> (request) { request.get? } before do - allow_access_with_scope :read_user if request.get? authenticate! end diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb index 3933c3b04dd..ccb5d886bab 100644 --- a/lib/gitlab/auth.rb +++ b/lib/gitlab/auth.rb @@ -130,13 +130,13 @@ module Gitlab token = PersonalAccessTokensFinder.new(state: 'active').find_by(token: password) - if token && valid_scoped_token?(token, AVAILABLE_SCOPES.map(&:to_s)) + if token && valid_scoped_token?(token, AVAILABLE_SCOPES) Gitlab::Auth::Result.new(token.user, nil, :personal_token, abilities_for_scope(token.scopes)) end end def valid_oauth_token?(token) - token && token.accessible? && valid_scoped_token?(token, ["api"]) + token && token.accessible? && valid_scoped_token?(token, [:api]) end def valid_scoped_token?(token, scopes) diff --git a/lib/gitlab/git/blob.rb b/lib/gitlab/git/blob.rb index a7aceab4c14..ffe4f3ca95f 100644 --- a/lib/gitlab/git/blob.rb +++ b/lib/gitlab/git/blob.rb @@ -175,6 +175,10 @@ module Gitlab encode! @name end + def path + encode! @path + end + def truncated? size && (size > loaded_size) end diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 23d0c8a9bdb..dd5a4d5ad55 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -554,11 +554,14 @@ module Gitlab # # => git@localhost:rack.git # def submodule_url_for(ref, path) - if submodules(ref).any? - submodule = submodules(ref)[path] - - if submodule - submodule['url'] + Gitlab::GitalyClient.migrate(:submodule_url_for) do |is_enabled| + if is_enabled + gitaly_submodule_url_for(ref, path) + else + if submodules(ref).any? + submodule = submodules(ref)[path] + submodule['url'] if submodule + end end end end @@ -915,6 +918,18 @@ module Gitlab fill_submodule_ids(commit, parser.parse) end + def gitaly_submodule_url_for(ref, path) + # We don't care about the contents so 1 byte is enough. Can't request 0 bytes, 0 means unlimited. + commit_object = gitaly_commit_client.tree_entry(ref, path, 1) + + return unless commit_object && commit_object.type == :COMMIT + + gitmodules = gitaly_commit_client.tree_entry(ref, '.gitmodules', Blob::MAX_DATA_DISPLAY_SIZE) + found_module = GitmodulesParser.new(gitmodules.data).parse[path] + + found_module && found_module['url'] + end + def alternate_object_directories Gitlab::Git::Env.all.values_at(*ALLOWED_OBJECT_DIRECTORIES_VARIABLES).compact end diff --git a/lib/gitlab/git/tree.rb b/lib/gitlab/git/tree.rb index b9afa05c819..b6d4e6cfe46 100644 --- a/lib/gitlab/git/tree.rb +++ b/lib/gitlab/git/tree.rb @@ -80,6 +80,10 @@ module Gitlab encode! @name end + def path + encode! @path + end + def dir? type == :tree end diff --git a/lib/gitlab/gitaly_client/ref.rb b/lib/gitlab/gitaly_client/ref.rb index 2d61992f595..6edc69de078 100644 --- a/lib/gitlab/gitaly_client/ref.rb +++ b/lib/gitlab/gitaly_client/ref.rb @@ -34,7 +34,7 @@ module Gitlab commit_id: commit_id, prefix: ref_prefix ) - GitalyClient.call(@storage, :ref, :find_ref_name, request).name + encode!(GitalyClient.call(@storage, :ref, :find_ref_name, request).name.dup) end def count_tag_names diff --git a/lib/gitlab/health_checks/fs_shards_check.rb b/lib/gitlab/health_checks/fs_shards_check.rb index e78b7f22e03..70da4080cae 100644 --- a/lib/gitlab/health_checks/fs_shards_check.rb +++ b/lib/gitlab/health_checks/fs_shards_check.rb @@ -52,7 +52,7 @@ module Gitlab ] end rescue RuntimeError => ex - Rails.logger("unexpected error #{ex} when checking #{ok_metric}") + Rails.logger.error("unexpected error #{ex} when checking #{ok_metric}") [metric(ok_metric, 0, **labels)] end diff --git a/lib/gitlab/i18n.rb b/lib/gitlab/i18n.rb index db7cdf4b5c7..f3d489aad0d 100644 --- a/lib/gitlab/i18n.rb +++ b/lib/gitlab/i18n.rb @@ -12,7 +12,8 @@ module Gitlab 'zh_HK' => '繁體中文(香港)', 'zh_TW' => '繁體中文(臺灣)', 'bg' => 'български', - 'eo' => 'Esperanto' + 'eo' => 'Esperanto', + 'it' => 'Italiano' }.freeze def available_locales diff --git a/lib/gitlab/metrics/base_sampler.rb b/lib/gitlab/metrics/base_sampler.rb new file mode 100644 index 00000000000..219accfc029 --- /dev/null +++ b/lib/gitlab/metrics/base_sampler.rb @@ -0,0 +1,94 @@ +require 'logger' +module Gitlab + module Metrics + class BaseSampler + def self.initialize_instance(*args) + raise "#{name} singleton instance already initialized" if @instance + @instance = new(*args) + at_exit(&@instance.method(:stop)) + @instance + end + + def self.instance + @instance + end + + attr_reader :running + + # interval - The sampling interval in seconds. + def initialize(interval) + interval_half = interval.to_f / 2 + + @interval = interval + @interval_steps = (-interval_half..interval_half).step(0.1).to_a + + @mutex = Mutex.new + end + + def enabled? + true + end + + def start + return unless enabled? + + @mutex.synchronize do + return if running + @running = true + + @thread = Thread.new do + sleep(sleep_interval) + + while running + safe_sample + + sleep(sleep_interval) + end + end + end + end + + def stop + @mutex.synchronize do + return unless running + + @running = false + + if @thread + @thread.wakeup if @thread.alive? + @thread.join + @thread = nil + end + end + end + + def safe_sample + sample + rescue => e + Rails.logger.warn("#{self.class}: #{e}, stopping") + stop + end + + def sample + raise NotImplementedError + end + + # Returns the sleep interval with a random adjustment. + # + # The random adjustment is put in place to ensure we: + # + # 1. Don't generate samples at the exact same interval every time (thus + # potentially missing anything that happens in between samples). + # 2. Don't sample data at the same interval two times in a row. + def sleep_interval + while step = @interval_steps.sample + if step != @last_step + @last_step = step + + return @interval + @last_step + end + end + end + end + end +end diff --git a/lib/gitlab/metrics/connection_rack_middleware.rb b/lib/gitlab/metrics/connection_rack_middleware.rb new file mode 100644 index 00000000000..b3da360be8f --- /dev/null +++ b/lib/gitlab/metrics/connection_rack_middleware.rb @@ -0,0 +1,45 @@ +module Gitlab + module Metrics + class ConnectionRackMiddleware + def initialize(app) + @app = app + end + + def self.rack_request_count + @rack_request_count ||= Gitlab::Metrics.counter(:rack_request, 'Rack request count') + end + + def self.rack_response_count + @rack_response_count ||= Gitlab::Metrics.counter(:rack_response, 'Rack response count') + end + + def self.rack_uncaught_errors_count + @rack_uncaught_errors_count ||= Gitlab::Metrics.counter(:rack_uncaught_errors, 'Rack connections handling uncaught errors count') + end + + def self.rack_execution_time + @rack_execution_time ||= Gitlab::Metrics.histogram(:rack_execution_time, 'Rack connection handling execution time', + {}, [0.05, 0.1, 0.25, 0.5, 0.7, 1, 1.5, 2, 2.5, 3, 5, 7, 10]) + end + + def call(env) + method = env['REQUEST_METHOD'].downcase + started = Time.now.to_f + begin + ConnectionRackMiddleware.rack_request_count.increment(method: method) + + status, headers, body = @app.call(env) + + ConnectionRackMiddleware.rack_response_count.increment(method: method, status: status) + [status, headers, body] + rescue + ConnectionRackMiddleware.rack_uncaught_errors_count.increment + raise + ensure + elapsed = Time.now.to_f - started + ConnectionRackMiddleware.rack_execution_time.observe({}, elapsed) + end + end + end + end +end diff --git a/lib/gitlab/metrics/sampler.rb b/lib/gitlab/metrics/influx_sampler.rb index 0000450d9bb..6db1dd755b7 100644 --- a/lib/gitlab/metrics/sampler.rb +++ b/lib/gitlab/metrics/influx_sampler.rb @@ -5,14 +5,11 @@ module Gitlab # This class is used to gather statistics that can't be directly associated # with a transaction such as system memory usage, garbage collection # statistics, etc. - class Sampler + class InfluxSampler < BaseSampler # interval - The sampling interval in seconds. def initialize(interval = Metrics.settings[:sample_interval]) - interval_half = interval.to_f / 2 - - @interval = interval - @interval_steps = (-interval_half..interval_half).step(0.1).to_a - @last_step = nil + super(interval) + @last_step = nil @metrics = [] @@ -26,18 +23,6 @@ module Gitlab end end - def start - Thread.new do - Thread.current.abort_on_exception = true - - loop do - sleep(sleep_interval) - - sample - end - end - end - def sample sample_memory_usage sample_file_descriptors @@ -86,7 +71,7 @@ module Gitlab end def sample_gc - time = GC::Profiler.total_time * 1000.0 + time = GC::Profiler.total_time * 1000.0 stats = GC.stat.merge(total_time: time) # We want the difference of GC runs compared to the last sample, not the @@ -111,23 +96,6 @@ module Gitlab def sidekiq? Sidekiq.server? end - - # Returns the sleep interval with a random adjustment. - # - # The random adjustment is put in place to ensure we: - # - # 1. Don't generate samples at the exact same interval every time (thus - # potentially missing anything that happens in between samples). - # 2. Don't sample data at the same interval two times in a row. - def sleep_interval - while step = @interval_steps.sample - if step != @last_step - @last_step = step - - return @interval + @last_step - end - end - end end end end diff --git a/lib/gitlab/metrics/prometheus.rb b/lib/gitlab/metrics/prometheus.rb index 9d314a56e58..fb7bbc7cfc7 100644 --- a/lib/gitlab/metrics/prometheus.rb +++ b/lib/gitlab/metrics/prometheus.rb @@ -29,8 +29,8 @@ module Gitlab provide_metric(name) || registry.summary(name, docstring, base_labels) end - def gauge(name, docstring, base_labels = {}) - provide_metric(name) || registry.gauge(name, docstring, base_labels) + def gauge(name, docstring, base_labels = {}, multiprocess_mode = :all) + provide_metric(name) || registry.gauge(name, docstring, base_labels, multiprocess_mode) end def histogram(name, docstring, base_labels = {}, buckets = ::Prometheus::Client::Histogram::DEFAULT_BUCKETS) diff --git a/lib/gitlab/metrics/unicorn_sampler.rb b/lib/gitlab/metrics/unicorn_sampler.rb new file mode 100644 index 00000000000..f6987252039 --- /dev/null +++ b/lib/gitlab/metrics/unicorn_sampler.rb @@ -0,0 +1,48 @@ +module Gitlab + module Metrics + class UnicornSampler < BaseSampler + def initialize(interval) + super(interval) + end + + def unicorn_active_connections + @unicorn_active_connections ||= Gitlab::Metrics.gauge(:unicorn_active_connections, 'Unicorn active connections', {}, :max) + end + + def unicorn_queued_connections + @unicorn_queued_connections ||= Gitlab::Metrics.gauge(:unicorn_queued_connections, 'Unicorn queued connections', {}, :max) + end + + def enabled? + # Raindrops::Linux.tcp_listener_stats is only present on Linux + unicorn_with_listeners? && Raindrops::Linux.respond_to?(:tcp_listener_stats) + end + + def sample + Raindrops::Linux.tcp_listener_stats(tcp_listeners).each do |addr, stats| + unicorn_active_connections.set({ type: 'tcp', address: addr }, stats.active) + unicorn_queued_connections.set({ type: 'tcp', address: addr }, stats.queued) + end + + Raindrops::Linux.unix_listener_stats(unix_listeners).each do |addr, stats| + unicorn_active_connections.set({ type: 'unix', address: addr }, stats.active) + unicorn_queued_connections.set({ type: 'unix', address: addr }, stats.queued) + end + end + + private + + def tcp_listeners + @tcp_listeners ||= Unicorn.listener_names.grep(%r{\A[^/]+:\d+\z}) + end + + def unix_listeners + @unix_listeners ||= Unicorn.listener_names - tcp_listeners + end + + def unicorn_with_listeners? + defined?(Unicorn) && Unicorn.listener_names.any? + end + end + end +end diff --git a/lib/gitlab/shell.rb b/lib/gitlab/shell.rb index 22554236c38..0baea092e6a 100644 --- a/lib/gitlab/shell.rb +++ b/lib/gitlab/shell.rb @@ -2,6 +2,8 @@ require 'securerandom' module Gitlab class Shell + GITLAB_SHELL_ENV_VARS = %w(GIT_TERMINAL_PROMPT).freeze + Error = Class.new(StandardError) KeyAdder = Struct.new(:io) do @@ -67,8 +69,8 @@ module Gitlab # add_repository("/path/to/storage", "gitlab/gitlab-ci") # def add_repository(storage, name) - Gitlab::Utils.system_silent([gitlab_shell_projects_path, - 'add-project', storage, "#{name}.git"]) + gitlab_shell_fast_execute([gitlab_shell_projects_path, + 'add-project', storage, "#{name}.git"]) end # Import repository @@ -82,10 +84,9 @@ module Gitlab def import_repository(storage, name, url) # Timeout should be less than 900 ideally, to prevent the memory killer # to silently kill the process without knowing we are timing out here. - output, status = Popen.popen([gitlab_shell_projects_path, 'import-project', - storage, "#{name}.git", url, "#{Gitlab.config.gitlab_shell.git_timeout}"]) - raise Error, output unless status.zero? - true + cmd = [gitlab_shell_projects_path, 'import-project', + storage, "#{name}.git", url, "#{Gitlab.config.gitlab_shell.git_timeout}"] + gitlab_shell_fast_execute_raise_error(cmd) end # Fetch remote for repository @@ -103,9 +104,7 @@ module Gitlab args << '--force' if forced args << '--no-tags' if no_tags - output, status = Popen.popen(args) - raise Error, output unless status.zero? - true + gitlab_shell_fast_execute_raise_error(args) end # Move repository @@ -117,8 +116,8 @@ module Gitlab # mv_repository("/path/to/storage", "gitlab/gitlab-ci", "randx/gitlab-ci-new") # def mv_repository(storage, path, new_path) - Gitlab::Utils.system_silent([gitlab_shell_projects_path, 'mv-project', - storage, "#{path}.git", "#{new_path}.git"]) + gitlab_shell_fast_execute([gitlab_shell_projects_path, 'mv-project', + storage, "#{path}.git", "#{new_path}.git"]) end # Fork repository to new namespace @@ -131,9 +130,9 @@ module Gitlab # fork_repository("/path/to/forked_from/storage", "gitlab/gitlab-ci", "/path/to/forked_to/storage", "randx") # def fork_repository(forked_from_storage, path, forked_to_storage, fork_namespace) - Gitlab::Utils.system_silent([gitlab_shell_projects_path, 'fork-project', - forked_from_storage, "#{path}.git", forked_to_storage, - fork_namespace]) + gitlab_shell_fast_execute([gitlab_shell_projects_path, 'fork-project', + forked_from_storage, "#{path}.git", forked_to_storage, + fork_namespace]) end # Remove repository from file system @@ -145,8 +144,8 @@ module Gitlab # remove_repository("/path/to/storage", "gitlab/gitlab-ci") # def remove_repository(storage, name) - Gitlab::Utils.system_silent([gitlab_shell_projects_path, - 'rm-project', storage, "#{name}.git"]) + gitlab_shell_fast_execute([gitlab_shell_projects_path, + 'rm-project', storage, "#{name}.git"]) end # Add new key to gitlab-shell @@ -155,8 +154,8 @@ module Gitlab # add_key("key-42", "sha-rsa ...") # def add_key(key_id, key_content) - Gitlab::Utils.system_silent([gitlab_shell_keys_path, - 'add-key', key_id, self.class.strip_key(key_content)]) + gitlab_shell_fast_execute([gitlab_shell_keys_path, + 'add-key', key_id, self.class.strip_key(key_content)]) end # Batch-add keys to authorized_keys @@ -175,8 +174,10 @@ module Gitlab # remove_key("key-342", "sha-rsa ...") # def remove_key(key_id, key_content) - Gitlab::Utils.system_silent([gitlab_shell_keys_path, - 'rm-key', key_id, key_content]) + args = [gitlab_shell_keys_path, 'rm-key', key_id] + args << key_content if key_content + + gitlab_shell_fast_execute(args) end # Remove all ssh keys from gitlab shell @@ -185,7 +186,7 @@ module Gitlab # remove_all_keys # def remove_all_keys - Gitlab::Utils.system_silent([gitlab_shell_keys_path, 'clear']) + gitlab_shell_fast_execute([gitlab_shell_keys_path, 'clear']) end # Add empty directory for storing repositories @@ -267,5 +268,31 @@ module Gitlab def gitlab_shell_keys_path File.join(gitlab_shell_path, 'bin', 'gitlab-keys') end + + private + + def gitlab_shell_fast_execute(cmd) + output, status = gitlab_shell_fast_execute_helper(cmd) + + return true if status.zero? + + Rails.logger.error("gitlab-shell failed with error #{status}: #{output}") + false + end + + def gitlab_shell_fast_execute_raise_error(cmd) + output, status = gitlab_shell_fast_execute_helper(cmd) + + raise Error, output unless status.zero? + true + end + + def gitlab_shell_fast_execute_helper(cmd) + vars = ENV.to_h.slice(*GITLAB_SHELL_ENV_VARS) + + # Don't pass along the entire parent environment to prevent gitlab-shell + # from wasting I/O by searching through GEM_PATH + Bundler.with_original_env { Popen.popen(cmd, nil, vars) } + end end end diff --git a/lib/tasks/gitlab/info.rake b/lib/tasks/gitlab/info.rake index e3883278886..e9fb6a008b0 100644 --- a/lib/tasks/gitlab/info.rake +++ b/lib/tasks/gitlab/info.rake @@ -42,8 +42,7 @@ namespace :gitlab do http_clone_url = project.http_url_to_repo ssh_clone_url = project.ssh_url_to_repo - omniauth_providers = Gitlab.config.omniauth.providers - omniauth_providers.map! { |provider| provider['name'] } + omniauth_providers = Gitlab.config.omniauth.providers.map { |provider| provider['name'] } puts "" puts "GitLab information".color(:yellow) diff --git a/locale/en/gitlab.po b/locale/en/gitlab.po index afb8fb3176f..bda3fc09e85 100644 --- a/locale/en/gitlab.po +++ b/locale/en/gitlab.po @@ -17,9 +17,27 @@ msgstr "" "Plural-Forms: nplurals=2; plural=n != 1;\n" "\n" +msgid "%d additional commit has been omitted to prevent performance issues." +msgid_plural "%d additional commits have been omitted to prevent performance issues." +msgstr[0] "" +msgstr[1] "" + +msgid "%d commit" +msgid_plural "%d commits" +msgstr[0] "" +msgstr[1] "" + msgid "%{commit_author_link} committed %{commit_timeago}" msgstr "" +msgid "1 pipeline" +msgid_plural "%d pipelines" +msgstr[0] "" +msgstr[1] "" + +msgid "A collection of graphs regarding Continuous Integration" +msgstr "" + msgid "About auto deploy" msgstr "" @@ -61,9 +79,24 @@ msgstr[1] "" msgid "Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}" msgstr "" +msgid "BranchSwitcherPlaceholder|Search branches" +msgstr "" + +msgid "BranchSwitcherTitle|Switch branch" +msgstr "" + msgid "Branches" msgstr "" +msgid "Browse Directory" +msgstr "" + +msgid "Browse File" +msgstr "" + +msgid "Browse Files" +msgstr "" + msgid "Browse files" msgstr "" @@ -159,6 +192,9 @@ msgid_plural "Commits" msgstr[0] "" msgstr[1] "" +msgid "Commit duration in minutes for last 30 commits" +msgstr "" + msgid "Commit message" msgstr "" @@ -171,6 +207,9 @@ msgstr "" msgid "Commits" msgstr "" +msgid "Commits feed" +msgstr "" + msgid "Commits|History" msgstr "" @@ -195,6 +234,9 @@ msgstr "" msgid "Create New Directory" msgstr "" +msgid "Create a personal access token on your account to pull or push via %{protocol}." +msgstr "" + msgid "Create directory" msgstr "" @@ -213,6 +255,9 @@ msgstr "" msgid "CreateTag|Tag" msgstr "" +msgid "CreateTokenToCloneLink|create a personal access token" +msgstr "" + msgid "Cron Timezone" msgstr "" @@ -323,6 +368,9 @@ msgstr "" msgid "Files" msgstr "" +msgid "Filter by commit message" +msgstr "" + msgid "Find by path" msgstr "" @@ -370,6 +418,15 @@ msgstr "" msgid "Introducing Cycle Analytics" msgstr "" +msgid "Jobs for last month" +msgstr "" + +msgid "Jobs for last week" +msgstr "" + +msgid "Jobs for last year" +msgstr "" + msgid "LFSStatus|Disabled" msgstr "" @@ -535,6 +592,21 @@ msgstr "" msgid "Pipeline Schedules" msgstr "" +msgid "PipelineCharts|Failed:" +msgstr "" + +msgid "PipelineCharts|Overall statistics" +msgstr "" + +msgid "PipelineCharts|Success ratio:" +msgstr "" + +msgid "PipelineCharts|Successful:" +msgstr "" + +msgid "PipelineCharts|Total:" +msgstr "" + msgid "PipelineSchedules|Activated" msgstr "" @@ -565,6 +637,18 @@ msgstr "" msgid "PipelineSheduleIntervalPattern|Custom" msgstr "" +msgid "Pipelines" +msgstr "" + +msgid "Pipelines charts" +msgstr "" + +msgid "Pipeline|all" +msgstr "" + +msgid "Pipeline|success" +msgstr "" + msgid "Pipeline|with stage" msgstr "" @@ -688,7 +772,7 @@ msgstr "" msgid "Select target branch" msgstr "" -msgid "Set a password on your account to pull or push via %{protocol}" +msgid "Set a password on your account to pull or push via %{protocol}." msgstr "" msgid "Set up CI" @@ -714,10 +798,7 @@ msgstr "" msgid "StarProject|Star" msgstr "" -msgid "Start a %{new_merge_request} with these changes" -msgstr "" - -msgid "Start a <strong>new merge request</strong> with these changes" +msgid "Start a %{new_merge_request} with these changes" msgstr "" msgid "Switch branch/tag" @@ -948,9 +1029,15 @@ msgstr "" msgid "Upload file" msgstr "" +msgid "UploadLink|click to upload" +msgstr "" + msgid "Use your global notification setting" msgstr "" +msgid "View open merge request" +msgstr "" + msgid "VisibilityLevel|Internal" msgstr "" diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 07f9efeb495..9f1caeddaa7 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -8,8 +8,8 @@ msgid "" msgstr "" "Project-Id-Version: gitlab 1.0.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-06-19 15:50-0500\n" -"PO-Revision-Date: 2017-06-19 15:50-0500\n" +"POT-Creation-Date: 2017-06-28 13:32+0200\n" +"PO-Revision-Date: 2017-06-28 13:32+0200\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" "Language: \n" @@ -18,7 +18,7 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" -msgid "%d additional commit have been omitted to prevent performance issues." +msgid "%d additional commit has been omitted to prevent performance issues." msgid_plural "%d additional commits have been omitted to prevent performance issues." msgstr[0] "" msgstr[1] "" @@ -31,6 +31,14 @@ msgstr[1] "" msgid "%{commit_author_link} committed %{commit_timeago}" msgstr "" +msgid "1 pipeline" +msgid_plural "%d pipelines" +msgstr[0] "" +msgstr[1] "" + +msgid "A collection of graphs regarding Continuous Integration" +msgstr "" + msgid "About auto deploy" msgstr "" @@ -185,6 +193,9 @@ msgid_plural "Commits" msgstr[0] "" msgstr[1] "" +msgid "Commit duration in minutes for last 30 commits" +msgstr "" + msgid "Commit message" msgstr "" @@ -224,6 +235,9 @@ msgstr "" msgid "Create New Directory" msgstr "" +msgid "Create a personal access token on your account to pull or push via %{protocol}." +msgstr "" + msgid "Create directory" msgstr "" @@ -242,6 +256,9 @@ msgstr "" msgid "CreateTag|Tag" msgstr "" +msgid "CreateTokenToCloneLink|create a personal access token" +msgstr "" + msgid "Cron Timezone" msgstr "" @@ -402,6 +419,15 @@ msgstr "" msgid "Introducing Cycle Analytics" msgstr "" +msgid "Jobs for last month" +msgstr "" + +msgid "Jobs for last week" +msgstr "" + +msgid "Jobs for last year" +msgstr "" + msgid "LFSStatus|Disabled" msgstr "" @@ -567,6 +593,21 @@ msgstr "" msgid "Pipeline Schedules" msgstr "" +msgid "PipelineCharts|Failed:" +msgstr "" + +msgid "PipelineCharts|Overall statistics" +msgstr "" + +msgid "PipelineCharts|Success ratio:" +msgstr "" + +msgid "PipelineCharts|Successful:" +msgstr "" + +msgid "PipelineCharts|Total:" +msgstr "" + msgid "PipelineSchedules|Activated" msgstr "" @@ -597,6 +638,18 @@ msgstr "" msgid "PipelineSheduleIntervalPattern|Custom" msgstr "" +msgid "Pipelines" +msgstr "" + +msgid "Pipelines charts" +msgstr "" + +msgid "Pipeline|all" +msgstr "" + +msgid "Pipeline|success" +msgstr "" + msgid "Pipeline|with stage" msgstr "" @@ -720,7 +773,7 @@ msgstr "" msgid "Select target branch" msgstr "" -msgid "Set a password on your account to pull or push via %{protocol}" +msgid "Set a password on your account to pull or push via %{protocol}." msgstr "" msgid "Set up CI" diff --git a/locale/it/gitlab.po b/locale/it/gitlab.po new file mode 100644 index 00000000000..e3e2af142f5 --- /dev/null +++ b/locale/it/gitlab.po @@ -0,0 +1,1143 @@ +# Huang Tao <htve@outlook.com>, 2017. #zanata +# Paolo Falomo <info@paolofalomo.it>, 2017. #zanata +msgid "" +msgstr "" +"Project-Id-Version: gitlab 1.0.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-06-15 21:59-0500\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"PO-Revision-Date: 2017-07-02 10:32-0400\n" +"Last-Translator: Paolo Falomo <info@paolofalomo.it>\n" +"Language-Team: Italian\n" +"Language: it\n" +"X-Generator: Zanata 3.9.6\n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" + +msgid "%{commit_author_link} committed %{commit_timeago}" +msgstr "%{commit_author_link} ha committato %{commit_timeago}" + +msgid "About auto deploy" +msgstr "Riguardo il rilascio automatico" + +msgid "Active" +msgstr "Attivo" + +msgid "Activity" +msgstr "Attività" + +msgid "Add Changelog" +msgstr "Aggiungi Changelog" + +msgid "Add Contribution guide" +msgstr "Aggiungi Guida per contribuire" + +msgid "Add License" +msgstr "Aggiungi Licenza" + +msgid "Add an SSH key to your profile to pull or push via SSH." +msgstr "" +"Aggiungi una chiave SSH al tuo profilo per eseguire pull o push tramite SSH" + +msgid "Add new directory" +msgstr "Aggiungi una directory (cartella)" + +msgid "Archived project! Repository is read-only" +msgstr "Progetto archiviato! La Repository è sola-lettura" + +msgid "Are you sure you want to delete this pipeline schedule?" +msgstr "Sei sicuro di voler cancellare questa pipeline programmata?" + +msgid "Attach a file by drag & drop or %{upload_link}" +msgstr "" +"Aggiungi un file tramite trascina & rilascia ( drag & drop) o " +"%{upload_link}" + +msgid "Branch" +msgid_plural "Branches" +msgstr[0] "Branch" +msgstr[1] "Branches" + +msgid "" +"Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, " +"choose a GitLab CI Yaml template and commit your changes. " +"%{link_to_autodeploy_doc}" +msgstr "" +"La branch <strong>%{branch_name}</strong> è stata creata. Per impostare un " +"rilascio automatico scegli un template CI di Gitlab e committa le tue " +"modifiche %{link_to_autodeploy_doc}" + +msgid "Branches" +msgstr "Branches" + +msgid "Browse files" +msgstr "Guarda i files" + +msgid "ByAuthor|by" +msgstr "per" + +msgid "CI configuration" +msgstr "Configurazione CI (Integrazione Continua)" + +msgid "Cancel" +msgstr "Cancella" + +msgid "ChangeTypeActionLabel|Pick into branch" +msgstr "Preleva nella branch" + +msgid "ChangeTypeActionLabel|Revert in branch" +msgstr "Ripristina nella branch" + +msgid "ChangeTypeAction|Cherry-pick" +msgstr "Cherry-pick" + +msgid "ChangeTypeAction|Revert" +msgstr "Ripristina" + +msgid "Changelog" +msgstr "Changelog" + +msgid "Charts" +msgstr "Grafici" + +msgid "Cherry-pick this commit" +msgstr "Cherry-pick this commit" + +msgid "Cherry-pick this merge request" +msgstr "Cherry-pick questa richiesta di merge" + +msgid "CiStatusLabel|canceled" +msgstr "cancellato" + +msgid "CiStatusLabel|created" +msgstr "creato" + +msgid "CiStatusLabel|failed" +msgstr "fallito" + +msgid "CiStatusLabel|manual action" +msgstr "azione manuale" + +msgid "CiStatusLabel|passed" +msgstr "superata" + +msgid "CiStatusLabel|passed with warnings" +msgstr "superata con avvisi" + +msgid "CiStatusLabel|pending" +msgstr "in coda" + +msgid "CiStatusLabel|skipped" +msgstr "saltata" + +msgid "CiStatusLabel|waiting for manual action" +msgstr "in attesa di azione manuale" + +msgid "CiStatusText|blocked" +msgstr "bloccata" + +msgid "CiStatusText|canceled" +msgstr "cancellata" + +msgid "CiStatusText|created" +msgstr "creata" + +msgid "CiStatusText|failed" +msgstr "fallita" + +msgid "CiStatusText|manual" +msgstr "manuale" + +msgid "CiStatusText|passed" +msgstr "superata" + +msgid "CiStatusText|pending" +msgstr "in coda" + +msgid "CiStatusText|skipped" +msgstr "saltata" + +msgid "CiStatus|running" +msgstr "in corso" + +msgid "Commit" +msgid_plural "Commits" +msgstr[0] "Commit" +msgstr[1] "Commits" + +msgid "Commit message" +msgstr "Messaggio del commit" + +msgid "CommitBoxTitle|Commit" +msgstr "Commit" + +msgid "CommitMessage|Add %{file_name}" +msgstr "Aggiungi %{file_name}" + +msgid "Commits" +msgstr "Commits" + +msgid "Commits|History" +msgstr "Cronologia" + +msgid "Committed by" +msgstr "Committato da " + +msgid "Compare" +msgstr "Confronta" + +msgid "Contribution guide" +msgstr "Guida per contribuire" + +msgid "Contributors" +msgstr "Collaboratori" + +msgid "Copy URL to clipboard" +msgstr "Copia URL negli appunti" + +msgid "Copy commit SHA to clipboard" +msgstr "Copia l'SHA del commit negli appunti" + +msgid "Create New Directory" +msgstr "Crea una nuova cartella" + +msgid "Create directory" +msgstr "Crea cartella" + +msgid "Create empty bare repository" +msgstr "Crea una repository vuota" + +msgid "Create merge request" +msgstr "Crea una richiesta di merge" + +msgid "Create new..." +msgstr "Crea nuovo..." + +msgid "CreateNewFork|Fork" +msgstr "Fork" + +msgid "CreateTag|Tag" +msgstr "Tag" + +msgid "Cron Timezone" +msgstr "Cron Timezone" + +msgid "Cron syntax" +msgstr "Sintassi Cron" + +msgid "Custom notification events" +msgstr "Eventi-Notifica personalizzati" + +msgid "" +"Custom notification levels are the same as participating levels. With custom " +"notification levels you will also receive notifications for select events. " +"To find out more, check out %{notification_link}." +msgstr "" +"I livelli di notifica personalizzati sono uguali a quelli di partecipazione. " +"Con i livelli di notifica personalizzati riceverai anche notifiche per gli " +"eventi da te scelti %{notification_link}." + +msgid "Cycle Analytics" +msgstr "Statistiche Cicliche" + +msgid "" +"Cycle Analytics gives an overview of how much time it takes to go from idea " +"to production in your project." +msgstr "" +"L'Analisi Ciclica fornisce una panoramica sul tempo che trascorre tra l'idea " +"ed il rilascio in produzione del tuo progetto" + +msgid "CycleAnalyticsStage|Code" +msgstr "Codice" + +msgid "CycleAnalyticsStage|Issue" +msgstr "Issue" + +msgid "CycleAnalyticsStage|Plan" +msgstr "Pianificazione" + +msgid "CycleAnalyticsStage|Production" +msgstr "Produzione" + +msgid "CycleAnalyticsStage|Review" +msgstr "Revisione" + +msgid "CycleAnalyticsStage|Staging" +msgstr "Pre-rilascio" + +msgid "CycleAnalyticsStage|Test" +msgstr "Test" + +msgid "Define a custom pattern with cron syntax" +msgstr "Definisci un patter personalizzato mediante la sintassi cron" + +msgid "Delete" +msgstr "Elimina" + +msgid "Deploy" +msgid_plural "Deploys" +msgstr[0] "Rilascio" +msgstr[1] "Rilasci" + +msgid "Description" +msgstr "Descrizione" + +msgid "Directory name" +msgstr "Nome cartella" + +msgid "Don't show again" +msgstr "Non mostrare più" + +msgid "Download" +msgstr "Scarica" + +msgid "Download tar" +msgstr "Scarica tar" + +msgid "Download tar.bz2" +msgstr "Scarica tar.bz2" + +msgid "Download tar.gz" +msgstr "Scarica tar.gz" + +msgid "Download zip" +msgstr "Scarica zip" + +msgid "DownloadArtifacts|Download" +msgstr "Scarica" + +msgid "DownloadCommit|Email Patches" +msgstr "Email Patches" + +msgid "DownloadCommit|Plain Diff" +msgstr "Differenze" + +msgid "DownloadSource|Download" +msgstr "Scarica" + +msgid "Edit" +msgstr "Modifica" + +msgid "Edit Pipeline Schedule %{id}" +msgstr "Cambia programmazione della pipeline %{id}" + +msgid "Every day (at 4:00am)" +msgstr "Ogni giorno (alle 4 del mattino)" + +msgid "Every month (on the 1st at 4:00am)" +msgstr "Ogni primo giorno del mese (alle 4 del mattino)" + +msgid "Every week (Sundays at 4:00am)" +msgstr "Ogni settimana (Di domenica alle 4 del mattino)" + +msgid "Failed to change the owner" +msgstr "Impossibile cambiare owner" + +msgid "Failed to remove the pipeline schedule" +msgstr "Impossibile rimuovere la pipeline pianificata" + +msgid "Files" +msgstr "Files" + +msgid "Find by path" +msgstr "Trova in percorso" + +msgid "Find file" +msgstr "Trova file" + +msgid "FirstPushedBy|First" +msgstr "Primo" + +msgid "FirstPushedBy|pushed by" +msgstr "Push di" + +msgid "Fork" +msgid_plural "Forks" +msgstr[0] "Fork" +msgstr[1] "Forks" + +msgid "ForkedFromProjectPath|Forked from" +msgstr "Fork da" + +msgid "From issue creation until deploy to production" +msgstr "Dalla creazione di un issue fino al rilascio in produzione" + +msgid "From merge request merge until deploy to production" +msgstr "" +"Dalla richiesta di merge fino effettua il merge fino al rilascio in " +"produzione" + +msgid "Go to your fork" +msgstr "Vai il tuo fork" + +msgid "GoToYourFork|Fork" +msgstr "Fork" + +msgid "Home" +msgstr "Home" + +msgid "Housekeeping successfully started" +msgstr "Housekeeping iniziato con successo" + +msgid "Import repository" +msgstr "Importa repository" + +msgid "Interval Pattern" +msgstr "Intervallo di Pattern" + +msgid "Introducing Cycle Analytics" +msgstr "Introduzione delle Analisi Cicliche" + +msgid "LFSStatus|Disabled" +msgstr "Disabilitato" + +msgid "LFSStatus|Enabled" +msgstr "Abilitato" + +msgid "Last %d day" +msgid_plural "Last %d days" +msgstr[0] "L'ultimo %d giorno" +msgstr[1] "Gli ultimi %d giorni" + +msgid "Last Pipeline" +msgstr "Ultima Pipeline" + +msgid "Last Update" +msgstr "Ultimo Aggiornamento" + +msgid "Last commit" +msgstr "Ultimo Commit" + +msgid "Learn more in the" +msgstr "Leggi di più su" + +msgid "Learn more in the|pipeline schedules documentation" +msgstr "documentazione sulla pianificazione delle pipelines" + +msgid "Leave group" +msgstr "Abbandona il gruppo" + +msgid "Leave project" +msgstr "Abbandona il progetto" + +msgid "Limited to showing %d event at most" +msgid_plural "Limited to showing %d events at most" +msgstr[0] "Limita visualizzazione %d d'evento" +msgstr[1] "Limita visualizzazione %d di eventi" + +msgid "Median" +msgstr "Mediano" + +msgid "MissingSSHKeyWarningLink|add an SSH key" +msgstr "aggiungi una chiave SSH" + +msgid "New Issue" +msgid_plural "New Issues" +msgstr[0] "Nuovo Issue" +msgstr[1] "Nuovi Issues" + +msgid "New Pipeline Schedule" +msgstr "Nuova pianificazione Pipeline" + +msgid "New branch" +msgstr "Nuova Branch" + +msgid "New directory" +msgstr "Nuova directory" + +msgid "New file" +msgstr "Nuovo file" + +msgid "New issue" +msgstr "Nuovo Issue" + +msgid "New merge request" +msgstr "Nuova richiesta di merge" + +msgid "New schedule" +msgstr "Nuova pianficazione" + +msgid "New snippet" +msgstr "Nuovo snippet" + +msgid "New tag" +msgstr "Nuovo tag" + +msgid "No repository" +msgstr "Nessuna Repository" + +msgid "No schedules" +msgstr "Nessuna pianificazione" + +msgid "Not available" +msgstr "Non disponibile" + +msgid "Not enough data" +msgstr "Dati insufficienti " + +msgid "Notification events" +msgstr "Notifica eventi" + +msgid "NotificationEvent|Close issue" +msgstr "Chiudi issue" + +msgid "NotificationEvent|Close merge request" +msgstr "Chiudi richiesta di merge" + +msgid "NotificationEvent|Failed pipeline" +msgstr "Pipeline fallita" + +msgid "NotificationEvent|Merge merge request" +msgstr "Completa la richiesta di merge" + +msgid "NotificationEvent|New issue" +msgstr "Nuovo issue" + +msgid "NotificationEvent|New merge request" +msgstr "Nuova richiesta di merge" + +msgid "NotificationEvent|New note" +msgstr "Nuova nota" + +msgid "NotificationEvent|Reassign issue" +msgstr "Riassegna issue" + +msgid "NotificationEvent|Reassign merge request" +msgstr "Riassegna richiesta di Merge" + +msgid "NotificationEvent|Reopen issue" +msgstr "Riapri issue" + +msgid "NotificationEvent|Successful pipeline" +msgstr "Pipeline Completata" + +msgid "NotificationLevel|Custom" +msgstr "Personalizzato" + +msgid "NotificationLevel|Disabled" +msgstr "Disabilitato" + +msgid "NotificationLevel|Global" +msgstr "Globale" + +msgid "NotificationLevel|On mention" +msgstr "Se menzionato" + +msgid "NotificationLevel|Participate" +msgstr "Partecipa" + +msgid "NotificationLevel|Watch" +msgstr "Osserva" + +msgid "OfSearchInADropdown|Filter" +msgstr "Filtra" + +msgid "OpenedNDaysAgo|Opened" +msgstr "Aperto" + +msgid "Options" +msgstr "Opzioni" + +msgid "Owner" +msgstr "Owner" + +msgid "Pipeline" +msgstr "Pipeline" + +msgid "Pipeline Health" +msgstr "Stato della Pipeline" + +msgid "Pipeline Schedule" +msgstr "Pianificazione Pipeline" + +msgid "Pipeline Schedules" +msgstr "Pianificazione multipla Pipeline" + +msgid "PipelineSchedules|Activated" +msgstr "Attivata" + +msgid "PipelineSchedules|Active" +msgstr "Attiva" + +msgid "PipelineSchedules|All" +msgstr "Tutto" + +msgid "PipelineSchedules|Inactive" +msgstr "Inattiva" + +msgid "PipelineSchedules|Next Run" +msgstr "Prossima esecuzione" + +msgid "PipelineSchedules|None" +msgstr "Nessuna" + +msgid "PipelineSchedules|Provide a short description for this pipeline" +msgstr "Fornisci una breve descrizione per questa pipeline" + +msgid "PipelineSchedules|Take ownership" +msgstr "Prendi possesso" + +msgid "PipelineSchedules|Target" +msgstr "Target" + +msgid "PipelineSheduleIntervalPattern|Custom" +msgstr "Personalizzato" + +msgid "Pipeline|with stage" +msgstr "con stadio" + +msgid "Pipeline|with stages" +msgstr "con più stadi" + +msgid "Project '%{project_name}' queued for deletion." +msgstr "Il Progetto '%{project_name}' in coda di eliminazione." + +msgid "Project '%{project_name}' was successfully created." +msgstr "Il Progetto '%{project_name}' è stato creato con successo." + +msgid "Project '%{project_name}' was successfully updated." +msgstr "Il Progetto '%{project_name}' è stato aggiornato con successo." + +msgid "Project '%{project_name}' will be deleted." +msgstr "Il Progetto '%{project_name}' verrà eliminato" + +msgid "Project access must be granted explicitly to each user." +msgstr "L'accesso al progetto dev'esser fornito esplicitamente ad ogni utente" + +msgid "Project export could not be deleted." +msgstr "L'esportazione del progetto non può essere eliminata." + +msgid "Project export has been deleted." +msgstr "L'esportazione del progetto è stata eliminata." + +msgid "" +"Project export link has expired. Please generate a new export from your " +"project settings." +msgstr "" +"Il link d'esportazione del progetto è scaduto. Genera una nuova esportazione " +"dalle impostazioni del tuo progetto." + +msgid "Project export started. A download link will be sent by email." +msgstr "" +"Esportazione del progetto iniziata. Un link di download sarà inviato via " +"email." + +msgid "Project home" +msgstr "Home di progetto" + +msgid "ProjectFeature|Disabled" +msgstr "Disabilitato" + +msgid "ProjectFeature|Everyone with access" +msgstr "Chiunque con accesso" + +msgid "ProjectFeature|Only team members" +msgstr "Solo i membri del team" + +msgid "ProjectFileTree|Name" +msgstr "Nome" + +msgid "ProjectLastActivity|Never" +msgstr "Mai" + +msgid "ProjectLifecycle|Stage" +msgstr "Stadio" + +msgid "ProjectNetworkGraph|Graph" +msgstr "Grafico" + +msgid "Read more" +msgstr "Continua..." + +msgid "Readme" +msgstr "Leggimi" + +msgid "RefSwitcher|Branches" +msgstr "Branches" + +msgid "RefSwitcher|Tags" +msgstr "Tags" + +msgid "Related Commits" +msgstr "Commit correlati" + +msgid "Related Deployed Jobs" +msgstr "Attività di Rilascio Correlate" + +msgid "Related Issues" +msgstr "Issues Correlati" + +msgid "Related Jobs" +msgstr "Attività Correlate" + +msgid "Related Merge Requests" +msgstr "Richieste di Merge Correlate" + +msgid "Related Merged Requests" +msgstr "Richieste di Merge Completate Correlate" + +msgid "Remind later" +msgstr "Ricordamelo più tardi" + +msgid "Remove project" +msgstr "Rimuovi progetto" + +msgid "Request Access" +msgstr "Richiedi accesso" + +msgid "Revert this commit" +msgstr "Ripristina questo commit" + +msgid "Revert this merge request" +msgstr "Ripristina questa richiesta di merge" + +msgid "Save pipeline schedule" +msgstr "Salva pianificazione pipeline" + +msgid "Schedule a new pipeline" +msgstr "Pianifica una nuova Pipeline" + +msgid "Scheduling Pipelines" +msgstr "Pianificazione pipelines" + +msgid "Search branches and tags" +msgstr "Ricerca branches e tags" + +msgid "Select Archive Format" +msgstr "Seleziona formato d'archivio" + +msgid "Select a timezone" +msgstr "Seleziona una timezone" + +msgid "Select target branch" +msgstr "Seleziona una branch di destinazione" + +msgid "Set a password on your account to pull or push via %{protocol}" +msgstr "" +"Imposta una password sul tuo account per eseguire pull o push tramite " +"%{protocol}" + +msgid "Set up CI" +msgstr "Configura CI" + +msgid "Set up Koding" +msgstr "Configura Koding" + +msgid "Set up auto deploy" +msgstr "Configura il rilascio automatico" + +msgid "SetPasswordToCloneLink|set a password" +msgstr "imposta una password" + +msgid "Showing %d event" +msgid_plural "Showing %d events" +msgstr[0] "Visualizza %d evento" +msgstr[1] "Visualizza %d eventi" + +msgid "Source code" +msgstr "Codice Sorgente" + +msgid "StarProject|Star" +msgstr "Star" + +msgid "Start a %{new_merge_request} with these changes" +msgstr "inizia una %{new_merge_request} con queste modifiche" + +msgid "Switch branch/tag" +msgstr "Cambia branch/tag" + +msgid "Tag" +msgid_plural "Tags" +msgstr[0] "Tag" +msgstr[1] "Tags" + +msgid "Tags" +msgstr "Tags" + +msgid "Target Branch" +msgstr "Branch di destinazione" + +msgid "" +"The coding stage shows the time from the first commit to creating the merge " +"request. The data will automatically be added here once you create your " +"first merge request." +msgstr "" +"Lo stadio di programmazione mostra il tempo trascorso dal primo commit alla " +"creazione di una richiesta di merge (MR). I dati saranno aggiunti una volta " +"che avrai creato la prima richiesta di merge." + +msgid "The collection of events added to the data gathered for that stage." +msgstr "L'insieme di eventi aggiunti ai dati raccolti per quello stadio." + +msgid "The fork relationship has been removed." +msgstr "La relazione del fork è stata rimossa" + +msgid "" +"The issue stage shows the time it takes from creating an issue to assigning " +"the issue to a milestone, or add the issue to a list on your Issue Board. " +"Begin creating issues to see data for this stage." +msgstr "" +"Questo stadio di issue mostra il tempo che ci vuole dal creare un issue " +"all'assegnarli una milestone, o ad aggiungere un issue alla tua board. Crea " +"un issue per vedere questo stadio." + +msgid "The phase of the development lifecycle." +msgstr "Il ciclo vitale della fase di sviluppo." + +msgid "" +"The pipelines schedule runs pipelines in the future, repeatedly, for " +"specific branches or tags. Those scheduled pipelines will inherit limited " +"project access based on their associated user." +msgstr "" +"Le pipelines pianificate vengono eseguite nel futuro, ripetitivamente, per " +"specifici tag o branch ed ereditano restrizioni di progetto basate " +"sull'utente ad esse associato." + +msgid "" +"The planning stage shows the time from the previous step to pushing your " +"first commit. This time will be added automatically once you push your first " +"commit." +msgstr "" +"Lo stadio di pianificazione mostra il tempo trascorso dal primo commit al " +"suo step precedente. Questo periodo sarà disponibile automaticamente nel " +"momento in cui farai il primo commit." + +msgid "" +"The production stage shows the total time it takes between creating an issue " +"and deploying the code to production. The data will be automatically added " +"once you have completed the full idea to production cycle." +msgstr "" +"Lo stadio di produzione mostra il tempo totale che trascorre tra la " +"creazione di un issue il suo rilascio (inteso come codice) in produzione. " +"Questo dato sarà disponibile automaticamente nel momento in cui avrai " +"completato l'intero processo ideale del ciclo di produzione" + +msgid "The project can be accessed by any logged in user." +msgstr "Qualunque utente autenticato può accedere a questo progetto." + +msgid "The project can be accessed without any authentication." +msgstr "" +"Chiunque può accedere a questo progetto (senza alcuna autenticazione)." + +msgid "The repository for this project does not exist." +msgstr "La repository di questo progetto non esiste." + +msgid "" +"The review stage shows the time from creating the merge request to merging " +"it. The data will automatically be added after you merge your first merge " +"request." +msgstr "" +"Lo stadio di revisione mostra il tempo tra una richiesta di merge al suo " +"svolgimento effettivo. Questo dato sarà disponibile appena avrai completato " +"una MR (Merger Request)" + +msgid "" +"The staging stage shows the time between merging the MR and deploying code " +"to the production environment. The data will be automatically added once you " +"deploy to production for the first time." +msgstr "" +"Lo stadio di pre-rilascio mostra il tempo che trascorre da una MR (Richiesta " +"di Merge) completata al suo rilascio in ambiente di produzione. Questa " +"informazione sarà disponibile dal tuo primo rilascio in produzione" + +msgid "" +"The testing stage shows the time GitLab CI takes to run every pipeline for " +"the related merge request. The data will automatically be added after your " +"first pipeline finishes running." +msgstr "" +"Lo stadio di test mostra il tempo che ogni Pipeline impiega per essere " +"eseguita in ogni Richiesta di Merge correlata. L'informazione sarà " +"disponibile automaticamente quando la tua prima Pipeline avrà finito d'esser " +"eseguita." + +msgid "The time taken by each data entry gathered by that stage." +msgstr "" +"Il tempo aggregato relativo eventi/data entry raccolto in quello stadio." + +msgid "" +"The value lying at the midpoint of a series of observed values. E.g., " +"between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 =" +" 6." +msgstr "" +"Il valore falsato nel mezzo di una serie di dati osservati. ES: tra 3,5,9 il " +"mediano è 5. Tra 3,5,7,8 il mediano è (5+7)/2 quindi 6." + +msgid "" +"This means you can not push code until you create an empty repository or " +"import existing one." +msgstr "" +"Questo significa che non è possibile effettuare push di codice fino a che " +"non crei una repository vuota o ne importi una esistente" + +msgid "Time before an issue gets scheduled" +msgstr "Il tempo che impiega un issue per esser pianificato" + +msgid "Time before an issue starts implementation" +msgstr "Il tempo che impiega un issue per esser implementato" + +msgid "Time between merge request creation and merge/close" +msgstr "Il tempo tra la creazione di una richiesta di merge ed il merge/close" + +msgid "Time until first merge request" +msgstr "Il tempo fino alla prima richiesta di merge" + +msgid "Timeago|%s days ago" +msgstr "%s giorni fa" + +msgid "Timeago|%s days remaining" +msgstr "%s giorni rimanenti" + +msgid "Timeago|%s hours remaining" +msgstr "%s ore rimanenti" + +msgid "Timeago|%s minutes ago" +msgstr "%s minuti fa" + +msgid "Timeago|%s minutes remaining" +msgstr "%s minuti rimanenti" + +msgid "Timeago|%s months ago" +msgstr "%s minuti fa" + +msgid "Timeago|%s months remaining" +msgstr "%s mesi rimanenti" + +msgid "Timeago|%s seconds remaining" +msgstr "%s secondi rimanenti" + +msgid "Timeago|%s weeks ago" +msgstr "%s settimane fa" + +msgid "Timeago|%s weeks remaining" +msgstr "%s settimane rimanenti" + +msgid "Timeago|%s years ago" +msgstr "%s anni fa" + +msgid "Timeago|%s years remaining" +msgstr "%s anni rimanenti" + +msgid "Timeago|1 day remaining" +msgstr "1 giorno rimanente" + +msgid "Timeago|1 hour remaining" +msgstr "1 ora rimanente" + +msgid "Timeago|1 minute remaining" +msgstr "1 minuto rimanente" + +msgid "Timeago|1 month remaining" +msgstr "1 mese rimanente" + +msgid "Timeago|1 week remaining" +msgstr "1 settimana rimanente" + +msgid "Timeago|1 year remaining" +msgstr "1 anno rimanente" + +msgid "Timeago|Past due" +msgstr "Entro" + +msgid "Timeago|a day ago" +msgstr "un giorno fa" + +msgid "Timeago|a month ago" +msgstr "un mese fa" + +msgid "Timeago|a week ago" +msgstr "una settimana fa" + +msgid "Timeago|a while" +msgstr "poco fa" + +msgid "Timeago|a year ago" +msgstr "un anno fa" + +msgid "Timeago|about %s hours ago" +msgstr "circa %s ore fa" + +msgid "Timeago|about a minute ago" +msgstr "circa un minuto fa" + +msgid "Timeago|about an hour ago" +msgstr "circa un ora fa" + +msgid "Timeago|in %s days" +msgstr "in %s giorni" + +msgid "Timeago|in %s hours" +msgstr "in %s ore" + +msgid "Timeago|in %s minutes" +msgstr "in %s minuti" + +msgid "Timeago|in %s months" +msgstr "in %s mesi" + +msgid "Timeago|in %s seconds" +msgstr "in %s secondi" + +msgid "Timeago|in %s weeks" +msgstr "in %s settimane" + +msgid "Timeago|in %s years" +msgstr "in %s anni" + +msgid "Timeago|in 1 day" +msgstr "in 1 giorno" + +msgid "Timeago|in 1 hour" +msgstr "in 1 ora" + +msgid "Timeago|in 1 minute" +msgstr "in 1 minuto" + +msgid "Timeago|in 1 month" +msgstr "in 1 mese" + +msgid "Timeago|in 1 week" +msgstr "in 1 settimana" + +msgid "Timeago|in 1 year" +msgstr "in 1 anno" + +msgid "Timeago|less than a minute ago" +msgstr "meno di un minuto fa" + +msgid "Time|hr" +msgid_plural "Time|hrs" +msgstr[0] "hr" +msgstr[1] "hr" + +msgid "Time|min" +msgid_plural "Time|mins" +msgstr[0] "min" +msgstr[1] "mins" + +msgid "Time|s" +msgstr "s" + +msgid "Total Time" +msgstr "Tempo Totale" + +msgid "Total test time for all commits/merges" +msgstr "Tempo totale di test per tutti i commits/merges" + +msgid "Unstar" +msgstr "Unstar" + +msgid "Upload New File" +msgstr "Carica un nuovo file" + +msgid "Upload file" +msgstr "Carica file" + +msgid "Use your global notification setting" +msgstr "Usa le tue impostazioni globali " + +msgid "VisibilityLevel|Internal" +msgstr "Interno" + +msgid "VisibilityLevel|Private" +msgstr "Privato" + +msgid "VisibilityLevel|Public" +msgstr "Pubblico" + +msgid "Want to see the data? Please ask an administrator for access." +msgstr "" +"Vuoi visualizzare i dati? Richiedi l'accesso ad un amministratore, grazie." + +msgid "We don't have enough data to show this stage." +msgstr "Non ci sono sufficienti dati da mostrare su questo stadio" + +msgid "Withdraw Access Request" +msgstr "Ritira richiesta d'accesso" + +msgid "" +"You are going to remove %{project_name_with_namespace}.\n" +"Removed project CANNOT be restored!\n" +"Are you ABSOLUTELY sure?" +msgstr "" +"Stai per rimuovere %{project_name_with_namespace}.\n" +"I progetti rimossi NON POSSONO essere ripristinati\n" +"Sei assolutamente sicuro?" + +msgid "" +"You are going to remove the fork relationship to source project " +"%{forked_from_project}. Are you ABSOLUTELY sure?" +msgstr "" +"Stai per rimuovere la relazione con il progetto sorgente " +"%{forked_from_project}. Sei ASSOLUTAMENTE sicuro?" + +msgid "" +"You are going to transfer %{project_name_with_namespace} to another owner. " +"Are you ABSOLUTELY sure?" +msgstr "" +"Stai per trasferire %{project_name_with_namespace} ad un altro owner. Sei " +"ASSOLUTAMENTE sicuro?" + +msgid "You can only add files when you are on a branch" +msgstr "Puoi aggiungere files solo quando sei in una branch" + +msgid "You have reached your project limit" +msgstr "Hai raggiunto il tuo limite di progetto" + +msgid "You must sign in to star a project" +msgstr "Devi accedere per porre una star al progetto" + +msgid "You need permission." +msgstr "Necessiti del permesso." + +msgid "You will not get any notifications via email" +msgstr "Non riceverai alcuna notifica via email" + +msgid "You will only receive notifications for the events you choose" +msgstr "Riceverai notifiche solo per gli eventi che hai scelto" + +msgid "" +"You will only receive notifications for threads you have participated in" +msgstr "Riceverai notifiche solo per i threads a cui hai partecipato" + +msgid "You will receive notifications for any activity" +msgstr "Riceverai notifiche per ogni attività" + +msgid "" +"You will receive notifications only for comments in which you were " +"@mentioned" +msgstr "Riceverai notifiche solo per i commenti ai quale sei stato menzionato" + +msgid "" +"You won't be able to pull or push project code via %{protocol} until you " +"%{set_password_link} on your account" +msgstr "" +"Non sarai in grado di eseguire pull o push di codice tramite %{protocol} " +"fino a che %{set_password_link} nel tuo account." + +msgid "" +"You won't be able to pull or push project code via SSH until you " +"%{add_ssh_key_link} to your profile" +msgstr "" +"Non sarai in grado di effettuare push o pull tramite SSH fino a che " +"%{add_ssh_key_link} al tuo profilo" + +msgid "Your name" +msgstr "Il tuo nome" + +msgid "day" +msgid_plural "days" +msgstr[0] "giorno" +msgstr[1] "giorni" + +msgid "new merge request" +msgstr "Nuova richiesta di merge" + +msgid "notification emails" +msgstr "Notifiche via email" + +msgid "parent" +msgid_plural "parents" +msgstr[0] "parent" +msgstr[1] "parents" + diff --git a/locale/it/gitlab.po.time_stamp b/locale/it/gitlab.po.time_stamp new file mode 100644 index 00000000000..e69de29bb2d --- /dev/null +++ b/locale/it/gitlab.po.time_stamp diff --git a/spec/controllers/groups/milestones_controller_spec.rb b/spec/controllers/groups/milestones_controller_spec.rb index f3263bc177d..c6e5fb61cf9 100644 --- a/spec/controllers/groups/milestones_controller_spec.rb +++ b/spec/controllers/groups/milestones_controller_spec.rb @@ -23,6 +23,21 @@ describe Groups::MilestonesController do project.team << [user, :master] end + describe "#index" do + it 'shows group milestones page' do + get :index, group_id: group.to_param + + expect(response).to have_http_status(200) + end + + it 'shows group milestones JSON' do + get :index, group_id: group.to_param, format: :json + + expect(response).to have_http_status(200) + expect(response.content_type).to eq 'application/json' + end + end + it_behaves_like 'milestone tabs' describe "#create" do diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb index 301c243febd..1c9595def21 100644 --- a/spec/features/boards/sidebar_spec.rb +++ b/spec/features/boards/sidebar_spec.rb @@ -79,6 +79,22 @@ describe 'Issue Boards', feature: true, js: true do end end + it 'does not show remove button for backlog or closed issues' do + create(:issue, project: project) + create(:issue, :closed, project: project) + + visit namespace_project_board_path(project.namespace, project, board) + wait_for_requests + + click_card(find('.board:nth-child(1)').first('.card')) + + expect(find('.issue-boards-sidebar')).not_to have_button 'Remove from board' + + click_card(find('.board:nth-child(3)').first('.card')) + + expect(find('.issue-boards-sidebar')).not_to have_button 'Remove from board' + end + context 'assignee' do it 'updates the issues assignee' do click_card(card) diff --git a/spec/features/dashboard/projects_spec.rb b/spec/features/dashboard/projects_spec.rb index f29186f368d..e9ef5d7983a 100644 --- a/spec/features/dashboard/projects_spec.rb +++ b/spec/features/dashboard/projects_spec.rb @@ -1,8 +1,8 @@ require 'spec_helper' -RSpec.describe 'Dashboard Projects', feature: true do +feature 'Dashboard Projects' do let(:user) { create(:user) } - let(:project) { create(:project, name: "awesome stuff") } + let(:project) { create(:project, name: 'awesome stuff') } let(:project2) { create(:project, :public, name: 'Community project') } before do @@ -15,6 +15,14 @@ RSpec.describe 'Dashboard Projects', feature: true do expect(page).to have_content('awesome stuff') end + it 'shows "New project" button' do + visit dashboard_projects_path + + page.within '#content-body' do + expect(page).to have_link('New project') + end + end + context 'when last_repository_updated_at, last_activity_at and update_at are present' do it 'shows the last_repository_updated_at attribute as the update date' do project.update_attributes!(last_repository_updated_at: Time.now, last_activity_at: 1.hour.ago) @@ -47,8 +55,8 @@ RSpec.describe 'Dashboard Projects', feature: true do end end - describe "with a pipeline", redis: true do - let!(:pipeline) { create(:ci_pipeline, project: project, sha: project.commit.sha) } + describe 'with a pipeline', redis: true do + let(:pipeline) { create(:ci_pipeline, project: project, sha: project.commit.sha) } before do # Since the cache isn't updated when a new pipeline is created diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb index ecacca00a61..c1dc7be7088 100644 --- a/spec/features/groups_spec.rb +++ b/spec/features/groups_spec.rb @@ -135,7 +135,7 @@ feature 'Group', feature: true do expect(page).not_to have_content('secret-group') end - describe 'group edit' do + describe 'group edit', js: true do let(:group) { create(:group) } let(:path) { edit_group_path(group) } let(:new_name) { 'new-name' } @@ -157,8 +157,8 @@ feature 'Group', feature: true do end it 'removes group' do - click_link 'Remove group' - + expect { remove_with_confirm('Remove group', group.path) }.to change {Group.count}.by(-1) + expect(group.members.all.count).to be_zero expect(page).to have_content "scheduled for deletion" end end @@ -212,4 +212,10 @@ feature 'Group', feature: true do expect(page).to have_content(nested_group.name) end end + + def remove_with_confirm(button_text, confirm_with) + click_button button_text + fill_in 'confirm_name_input', with: confirm_with + click_button 'Confirm' + end end diff --git a/spec/features/issues/form_spec.rb b/spec/features/issues/form_spec.rb index b369ef1ff79..58f6bd277e4 100644 --- a/spec/features/issues/form_spec.rb +++ b/spec/features/issues/form_spec.rb @@ -31,8 +31,8 @@ describe 'New/edit issue', :feature, :js do # the original method, resulting in infinite recurison when called. # This is likely a bug with helper modules included into dynamically generated view classes. # To work around this, we have to hold on to and call to the original implementation manually. - original_issue_dropdown_options = FormHelper.instance_method(:issue_dropdown_options) - allow_any_instance_of(FormHelper).to receive(:issue_dropdown_options).and_wrap_original do |original, *args| + original_issue_dropdown_options = FormHelper.instance_method(:issue_assignees_dropdown_options) + allow_any_instance_of(FormHelper).to receive(:issue_assignees_dropdown_options).and_wrap_original do |original, *args| options = original_issue_dropdown_options.bind(original.receiver).call(*args) options[:data][:per_page] = 2 diff --git a/spec/features/issues/move_spec.rb b/spec/features/issues/move_spec.rb index 21a7637fe7f..d6e78157e5f 100644 --- a/spec/features/issues/move_spec.rb +++ b/spec/features/issues/move_spec.rb @@ -41,13 +41,10 @@ feature 'issue move to another project' do find('#issuable-move', visible: false).set(new_project.id) click_button('Save changes') - wait_for_requests - - expect(current_url).to include project_path(new_project) - expect(page).to have_content("Text with #{cross_reference}#{mr.to_reference}") expect(page).to have_content("moved from #{cross_reference}#{issue.to_reference}") expect(page).to have_content(issue.title) + expect(page.current_path).to include project_path(new_project) end scenario 'searching project dropdown', js: true do diff --git a/spec/features/issues/update_issues_spec.rb b/spec/features/issues/update_issues_spec.rb index dc981406e4e..df704b55839 100644 --- a/spec/features/issues/update_issues_spec.rb +++ b/spec/features/issues/update_issues_spec.rb @@ -1,16 +1,16 @@ require 'rails_helper' -feature 'Multiple issue updating from issues#index', feature: true do +feature 'Multiple issue updating from issues#index', :js do let!(:project) { create(:project) } let!(:issue) { create(:issue, project: project) } let!(:user) { create(:user)} before do project.team << [user, :master] - gitlab_sign_in(user) + sign_in(user) end - context 'status', js: true do + context 'status' do it 'sets to closed' do visit namespace_project_issues_path(project.namespace, project) @@ -37,7 +37,7 @@ feature 'Multiple issue updating from issues#index', feature: true do end end - context 'assignee', js: true do + context 'assignee' do it 'updates to current user' do visit namespace_project_issues_path(project.namespace, project) @@ -67,8 +67,8 @@ feature 'Multiple issue updating from issues#index', feature: true do end end - context 'milestone', js: true do - let(:milestone) { create(:milestone, project: project) } + context 'milestone' do + let!(:milestone) { create(:milestone, project: project) } it 'updates milestone' do visit namespace_project_issues_path(project.namespace, project) diff --git a/spec/features/oauth_login_spec.rb b/spec/features/oauth_login_spec.rb new file mode 100644 index 00000000000..452b920307c --- /dev/null +++ b/spec/features/oauth_login_spec.rb @@ -0,0 +1,112 @@ +require 'spec_helper' + +feature 'OAuth Login', js: true do + def enter_code(code) + fill_in 'user_otp_attempt', with: code + click_button 'Verify code' + end + + def stub_omniauth_config(provider) + OmniAuth.config.add_mock(provider, OmniAuth::AuthHash.new(provider: provider.to_s, uid: "12345")) + Rails.application.env_config['devise.mapping'] = Devise.mappings[:user] + Rails.application.env_config['omniauth.auth'] = OmniAuth.config.mock_auth[provider] + end + + providers = [:github, :twitter, :bitbucket, :gitlab, :google_oauth2, + :facebook, :authentiq, :cas3, :auth0] + + before(:all) do + # The OmniAuth `full_host` parameter doesn't get set correctly (it gets set to something like `http://localhost` + # here), and causes integration tests to fail with 404s. We set the `full_host` by removing the request path (and + # anything after it) from the request URI. + @omniauth_config_full_host = OmniAuth.config.full_host + OmniAuth.config.full_host = ->(request) { request['REQUEST_URI'].sub(/#{request['REQUEST_PATH']}.*/, '') } + end + + after(:all) do + OmniAuth.config.full_host = @omniauth_config_full_host + end + + providers.each do |provider| + context "when the user logs in using the #{provider} provider" do + context 'when two-factor authentication is disabled' do + it 'logs the user in' do + stub_omniauth_config(provider) + user = create(:omniauth_user, extern_uid: 'my-uid', provider: provider.to_s) + login_via(provider.to_s, user, 'my-uid') + + expect(current_path).to eq root_path + end + end + + context 'when two-factor authentication is enabled' do + it 'logs the user in' do + stub_omniauth_config(provider) + user = create(:omniauth_user, :two_factor, extern_uid: 'my-uid', provider: provider.to_s) + login_via(provider.to_s, user, 'my-uid') + + enter_code(user.current_otp) + expect(current_path).to eq root_path + end + end + + context 'when "remember me" is checked' do + context 'when two-factor authentication is disabled' do + it 'remembers the user after a browser restart' do + stub_omniauth_config(provider) + user = create(:omniauth_user, extern_uid: 'my-uid', provider: provider.to_s) + login_via(provider.to_s, user, 'my-uid', remember_me: true) + + clear_browser_session + + visit(root_path) + expect(current_path).to eq root_path + end + end + + context 'when two-factor authentication is enabled' do + it 'remembers the user after a browser restart' do + stub_omniauth_config(provider) + user = create(:omniauth_user, :two_factor, extern_uid: 'my-uid', provider: provider.to_s) + login_via(provider.to_s, user, 'my-uid', remember_me: true) + enter_code(user.current_otp) + + clear_browser_session + + visit(root_path) + expect(current_path).to eq root_path + end + end + end + + context 'when "remember me" is not checked' do + context 'when two-factor authentication is disabled' do + it 'does not remember the user after a browser restart' do + stub_omniauth_config(provider) + user = create(:omniauth_user, extern_uid: 'my-uid', provider: provider.to_s) + login_via(provider.to_s, user, 'my-uid', remember_me: false) + + clear_browser_session + + visit(root_path) + expect(current_path).to eq new_user_session_path + end + end + + context 'when two-factor authentication is enabled' do + it 'does not remember the user after a browser restart' do + stub_omniauth_config(provider) + user = create(:omniauth_user, :two_factor, extern_uid: 'my-uid', provider: provider.to_s) + login_via(provider.to_s, user, 'my-uid', remember_me: false) + enter_code(user.current_otp) + + clear_browser_session + + visit(root_path) + expect(current_path).to eq new_user_session_path + end + end + end + end + end +end diff --git a/spec/features/projects/new_project_spec.rb b/spec/features/projects/new_project_spec.rb index 37d9a97033b..22fb1223739 100644 --- a/spec/features/projects/new_project_spec.rb +++ b/spec/features/projects/new_project_spec.rb @@ -1,13 +1,27 @@ -require "spec_helper" +require 'spec_helper' -feature "New project", feature: true do +feature 'New project' do let(:user) { create(:admin) } before do - gitlab_sign_in(user) + sign_in(user) end - context "Visibility level selector" do + it 'shows "New project" page' do + visit new_project_path + + expect(page).to have_content('Project path') + expect(page).to have_content('Project name') + + expect(page).to have_link('GitHub') + expect(page).to have_link('Bitbucket') + expect(page).to have_link('GitLab.com') + expect(page).to have_link('Google Code') + expect(page).to have_button('Repo by URL') + expect(page).to have_link('GitLab export') + end + + context 'Visibility level selector' do Gitlab::VisibilityLevel.options.each do |key, level| it "sets selector to #{key}" do stub_application_setting(default_project_visibility: level) @@ -28,20 +42,20 @@ feature "New project", feature: true do end end - context "Namespace selector" do - context "with user namespace" do + context 'Namespace selector' do + context 'with user namespace' do before do visit new_project_path end - it "selects the user namespace" do - namespace = find("#project_namespace_id") + it 'selects the user namespace' do + namespace = find('#project_namespace_id') expect(namespace.text).to eq user.username end end - context "with group namespace" do + context 'with group namespace' do let(:group) { create(:group, :private, owner: user) } before do @@ -49,13 +63,13 @@ feature "New project", feature: true do visit new_project_path(namespace_id: group.id) end - it "selects the group namespace" do - namespace = find("#project_namespace_id option[selected]") + it 'selects the group namespace' do + namespace = find('#project_namespace_id option[selected]') expect(namespace.text).to eq group.name end - context "on validation error" do + context 'on validation error' do before do fill_in('project_path', with: 'private-group-project') choose('Internal') @@ -64,15 +78,15 @@ feature "New project", feature: true do expect(page).to have_css '.project-edit-errors .alert.alert-danger' end - it "selects the group namespace" do - namespace = find("#project_namespace_id option[selected]") + it 'selects the group namespace' do + namespace = find('#project_namespace_id option[selected]') expect(namespace.text).to eq group.name end end end - context "with subgroup namespace" do + context 'with subgroup namespace' do let(:group) { create(:group, :private, owner: user) } let(:subgroup) { create(:group, parent: group) } @@ -81,8 +95,8 @@ feature "New project", feature: true do visit new_project_path(namespace_id: subgroup.id) end - it "selects the group namespace" do - namespace = find("#project_namespace_id option[selected]") + it 'selects the group namespace' do + namespace = find('#project_namespace_id option[selected]') expect(namespace.text).to eq subgroup.full_path end @@ -94,10 +108,45 @@ feature "New project", feature: true do visit new_project_path end - it 'does not autocomplete sensitive git repo URL' do - autocomplete = find('#project_import_url')['autocomplete'] + context 'from git repository url' do + before do + first('.import_git').click + end + + it 'does not autocomplete sensitive git repo URL' do + autocomplete = find('#project_import_url')['autocomplete'] + + expect(autocomplete).to eq('off') + end + + it 'shows import instructions' do + git_import_instructions = first('.js-toggle-content') - expect(autocomplete).to eq('off') + expect(git_import_instructions).to be_visible + expect(git_import_instructions).to have_content 'Git repository URL' + end + end + + context 'from GitHub' do + before do + first('.import_github').click + end + + it 'shows import instructions' do + expect(page).to have_content('Import Projects from GitHub') + expect(current_path).to eq new_import_github_path + end + end + + context 'from Google Code' do + before do + first('.import_google_code').click + end + + it 'shows import instructions' do + expect(page).to have_content('Import projects from Google Code') + expect(current_path).to eq new_import_google_code_path + end end end end diff --git a/spec/features/user_can_display_performance_bar_spec.rb b/spec/features/user_can_display_performance_bar_spec.rb index 1bd7e038939..24fff1a3052 100644 --- a/spec/features/user_can_display_performance_bar_spec.rb +++ b/spec/features/user_can_display_performance_bar_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -describe 'User can display performacne bar', :js do +describe 'User can display performance bar', :js do shared_examples 'performance bar is disabled' do it 'does not show the performance bar by default' do expect(page).not_to have_css('#peek') @@ -27,8 +27,8 @@ describe 'User can display performacne bar', :js do find('body').native.send_keys('pb') end - it 'does not show the performance bar by default' do - expect(page).not_to have_css('#peek') + it 'shows the performance bar' do + expect(page).to have_css('#peek') end end end diff --git a/spec/finders/labels_finder_spec.rb b/spec/finders/labels_finder_spec.rb index 1724cdba830..95d96354b77 100644 --- a/spec/finders/labels_finder_spec.rb +++ b/spec/finders/labels_finder_spec.rb @@ -49,12 +49,12 @@ describe LabelsFinder do end context 'filtering by group_id' do - it 'returns labels available for any project within the group' do + it 'returns labels available for any non-archived project within the group' do group_1.add_developer(user) - + project_1.archive! finder = described_class.new(user, group_id: group_1.id) - expect(finder.execute).to eq [group_label_2, project_label_1, group_label_1, project_label_5] + expect(finder.execute).to eq [group_label_2, group_label_1, project_label_5] end end diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index 56daeffde27..f161dbb4cf0 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -292,7 +292,7 @@ describe ApplicationHelper do let(:alternate_url) { 'http://company.example.com/getting-help' } before do - allow(current_application_settings).to receive(:help_page_support_url) { alternate_url } + stub_application_setting(help_page_support_url: alternate_url) end it 'returns the alternate support url' do diff --git a/spec/helpers/milestones_helper_spec.rb b/spec/helpers/milestones_helper_spec.rb index 3cb809d42b5..24d4f1b4938 100644 --- a/spec/helpers/milestones_helper_spec.rb +++ b/spec/helpers/milestones_helper_spec.rb @@ -1,6 +1,42 @@ require 'spec_helper' describe MilestonesHelper do + describe '#milestones_filter_dropdown_path' do + let(:project) { create(:empty_project) } + let(:project2) { create(:empty_project) } + let(:group) { create(:group) } + + context 'when @project present' do + it 'returns project milestones JSON URL' do + assign(:project, project) + + expect(helper.milestones_filter_dropdown_path).to eq(namespace_project_milestones_path(project.namespace, project, :json)) + end + end + + context 'when @target_project present' do + it 'returns targeted project milestones JSON URL' do + assign(:target_project, project2) + + expect(helper.milestones_filter_dropdown_path).to eq(namespace_project_milestones_path(project2.namespace, project2, :json)) + end + end + + context 'when @group present' do + it 'returns group milestones JSON URL' do + assign(:group, group) + + expect(helper.milestones_filter_dropdown_path).to eq(group_milestones_path(group, :json)) + end + end + + context 'when neither of @project/@target_project/@group present' do + it 'returns dashboard milestones JSON URL' do + expect(helper.milestones_filter_dropdown_path).to eq(dashboard_milestones_path(:json)) + end + end + end + describe "#milestone_date_range" do def result_for(*args) milestone_date_range(build(:milestone, *args)) diff --git a/spec/initializers/8_metrics_spec.rb b/spec/initializers/8_metrics_spec.rb index a507d7f7f2b..d4189f902fd 100644 --- a/spec/initializers/8_metrics_spec.rb +++ b/spec/initializers/8_metrics_spec.rb @@ -1,17 +1,25 @@ require 'spec_helper' -require_relative '../../config/initializers/8_metrics' describe 'instrument_classes', lib: true do let(:config) { double(:config) } + let(:unicorn_sampler) { double(:unicorn_sampler) } + let(:influx_sampler) { double(:influx_sampler) } + before do allow(config).to receive(:instrument_method) allow(config).to receive(:instrument_methods) allow(config).to receive(:instrument_instance_method) allow(config).to receive(:instrument_instance_methods) + allow(Gitlab::Metrics::UnicornSampler).to receive(:initialize_instance).and_return(unicorn_sampler) + allow(Gitlab::Metrics::InfluxSampler).to receive(:initialize_instance).and_return(influx_sampler) + allow(unicorn_sampler).to receive(:start) + allow(influx_sampler).to receive(:start) + allow(Gitlab::Application).to receive(:configure) end it 'can autoload and instrument all files' do + require_relative '../../config/initializers/8_metrics' expect { instrument_classes(config) }.not_to raise_error end end diff --git a/spec/javascripts/fixtures/oauth_remember_me.html.haml b/spec/javascripts/fixtures/oauth_remember_me.html.haml new file mode 100644 index 00000000000..7886e995e57 --- /dev/null +++ b/spec/javascripts/fixtures/oauth_remember_me.html.haml @@ -0,0 +1,5 @@ +#oauth-container + %input#remember_me{ type: "checkbox" } + + %a.oauth-login.twitter{ href: "http://example.com/" } + %a.oauth-login.github{ href: "http://example.com/" } diff --git a/spec/javascripts/monitoring/mock_data.js b/spec/javascripts/monitoring/mock_data.js index 6f4cb989847..56d938e1fbe 100644 --- a/spec/javascripts/monitoring/mock_data.js +++ b/spec/javascripts/monitoring/mock_data.js @@ -13,7 +13,7 @@ const metricsGroupsAPIResponse = { 'queries': [ { 'query_range': 'avg(container_memory_usage_bytes{%{environment_filter}}) / 2^20', - 'label': 'Container memory', + 'y_label': 'Memory', 'unit': 'MiB', 'result': [ { @@ -2477,7 +2477,7 @@ export const singleRowMetrics = [ { 'title': 'CPU usage', 'weight': 1, - 'y_label': 'Values', + 'y_label': 'Memory', 'queries': [ { 'query_range': 'avg(rate(container_cpu_usage_seconds_total{%{environment_filter}}[2m])) * 100', diff --git a/spec/javascripts/monitoring/monitoring_column_spec.js b/spec/javascripts/monitoring/monitoring_column_spec.js index c8787f9708c..b3bc97adc9f 100644 --- a/spec/javascripts/monitoring/monitoring_column_spec.js +++ b/spec/javascripts/monitoring/monitoring_column_spec.js @@ -94,4 +94,15 @@ describe('MonitoringColumn', () => { done(); }); }); + + it('has a title for the y-axis that comes from the backend', () => { + const component = createComponent({ + columnData: singleRowMetrics[0], + classType: 'col-md-6', + updateAspectRatio: false, + deploymentData, + }); + + expect(component.yAxisLabel).toEqual(component.columnData.y_label); + }); }); diff --git a/spec/javascripts/oauth_remember_me_spec.js b/spec/javascripts/oauth_remember_me_spec.js new file mode 100644 index 00000000000..f90e0093d25 --- /dev/null +++ b/spec/javascripts/oauth_remember_me_spec.js @@ -0,0 +1,26 @@ +import OAuthRememberMe from '~/oauth_remember_me'; + +describe('OAuthRememberMe', () => { + preloadFixtures('static/oauth_remember_me.html.raw'); + + beforeEach(() => { + loadFixtures('static/oauth_remember_me.html.raw'); + + new OAuthRememberMe({ container: $('#oauth-container') }).bindEvents(); + }); + + it('adds the "remember_me" query parameter to all OAuth login buttons', () => { + $('#oauth-container #remember_me').click(); + + expect($('#oauth-container .oauth-login.twitter').attr('href')).toBe('http://example.com/?remember_me=1'); + expect($('#oauth-container .oauth-login.github').attr('href')).toBe('http://example.com/?remember_me=1'); + }); + + it('removes the "remember_me" query parameter from all OAuth login buttons', () => { + $('#oauth-container #remember_me').click(); + $('#oauth-container #remember_me').click(); + + expect($('#oauth-container .oauth-login.twitter').attr('href')).toBe('http://example.com/'); + expect($('#oauth-container .oauth-login.github').attr('href')).toBe('http://example.com/'); + }); +}); diff --git a/spec/lib/gitlab/git/blame_spec.rb b/spec/lib/gitlab/git/blame_spec.rb index 8b041ac69b1..66c016d14b3 100644 --- a/spec/lib/gitlab/git/blame_spec.rb +++ b/spec/lib/gitlab/git/blame_spec.rb @@ -20,6 +20,7 @@ describe Gitlab::Git::Blame, seed_helper: true do expect(data.size).to eq(95) expect(data.first[:commit]).to be_kind_of(Gitlab::Git::Commit) expect(data.first[:line]).to eq("# Contribute to GitLab") + expect(data.first[:line]).to be_utf8 end end @@ -40,6 +41,7 @@ describe Gitlab::Git::Blame, seed_helper: true do expect(data.size).to eq(1) expect(data.first[:commit]).to be_kind_of(Gitlab::Git::Commit) expect(data.first[:line]).to eq("Ä ü") + expect(data.first[:line]).to be_utf8 end end @@ -61,6 +63,7 @@ describe Gitlab::Git::Blame, seed_helper: true do expect(data.size).to eq(1) expect(data.first[:commit]).to be_kind_of(Gitlab::Git::Commit) expect(data.first[:line]).to eq(" ") + expect(data.first[:line]).to be_utf8 end end end diff --git a/spec/lib/gitlab/git/branch_spec.rb b/spec/lib/gitlab/git/branch_spec.rb index 9dba4397e79..d1d7ed1d02a 100644 --- a/spec/lib/gitlab/git/branch_spec.rb +++ b/spec/lib/gitlab/git/branch_spec.rb @@ -48,7 +48,7 @@ describe Gitlab::Git::Branch, seed_helper: true do expect(Gitlab::Git::Commit).to receive(:decorate) .with(hash_including(attributes)).and_call_original - expect(branch.dereferenced_target.message.encoding).to be(Encoding::UTF_8) + expect(branch.dereferenced_target.message).to be_utf8 end end diff --git a/spec/lib/gitlab/git/diff_spec.rb b/spec/lib/gitlab/git/diff_spec.rb index d50ccb0df30..d97e85364c2 100644 --- a/spec/lib/gitlab/git/diff_spec.rb +++ b/spec/lib/gitlab/git/diff_spec.rb @@ -180,7 +180,7 @@ EOT let(:raw_patch) { @raw_diff_hash[:diff].encode(Encoding::ASCII_8BIT) } it 'encodes diff patch to UTF-8' do - expect(diff.diff.encoding).to eq(Encoding::UTF_8) + expect(diff.diff).to be_utf8 end end end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 0cd458bf933..6aca181194a 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -27,34 +27,24 @@ describe Gitlab::Git::Repository, seed_helper: true do end it 'returns UTF-8' do - expect(repository.root_ref.encoding).to eq(Encoding.find('UTF-8')) + expect(repository.root_ref).to be_utf8 end - context 'with gitaly enabled' do - before do - stub_gitaly - end - - after do - Gitlab::GitalyClient.clear_stubs! - end - - it 'gets the branch name from GitalyClient' do - expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:default_branch_name) - repository.root_ref - end + it 'gets the branch name from GitalyClient' do + expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:default_branch_name) + repository.root_ref + end - it 'wraps GRPC not found' do - expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:default_branch_name) - .and_raise(GRPC::NotFound) - expect { repository.root_ref }.to raise_error(Gitlab::Git::Repository::NoRepository) - end + it 'wraps GRPC not found' do + expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:default_branch_name) + .and_raise(GRPC::NotFound) + expect { repository.root_ref }.to raise_error(Gitlab::Git::Repository::NoRepository) + end - it 'wraps GRPC exceptions' do - expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:default_branch_name) - .and_raise(GRPC::Unknown) - expect { repository.root_ref }.to raise_error(Gitlab::Git::CommandError) - end + it 'wraps GRPC exceptions' do + expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:default_branch_name) + .and_raise(GRPC::Unknown) + expect { repository.root_ref }.to raise_error(Gitlab::Git::CommandError) end end @@ -129,37 +119,27 @@ describe Gitlab::Git::Repository, seed_helper: true do end it 'returns UTF-8' do - expect(subject.first.encoding).to eq(Encoding.find('UTF-8')) + expect(subject.first).to be_utf8 end it { is_expected.to include("master") } it { is_expected.not_to include("branch-from-space") } - context 'with gitaly enabled' do - before do - stub_gitaly - end - - after do - Gitlab::GitalyClient.clear_stubs! - end - - it 'gets the branch names from GitalyClient' do - expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:branch_names) - subject - end + it 'gets the branch names from GitalyClient' do + expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:branch_names) + subject + end - it 'wraps GRPC not found' do - expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:branch_names) - .and_raise(GRPC::NotFound) - expect { subject }.to raise_error(Gitlab::Git::Repository::NoRepository) - end + it 'wraps GRPC not found' do + expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:branch_names) + .and_raise(GRPC::NotFound) + expect { subject }.to raise_error(Gitlab::Git::Repository::NoRepository) + end - it 'wraps GRPC other exceptions' do - expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:branch_names) - .and_raise(GRPC::Unknown) - expect { subject }.to raise_error(Gitlab::Git::CommandError) - end + it 'wraps GRPC other exceptions' do + expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:branch_names) + .and_raise(GRPC::Unknown) + expect { subject }.to raise_error(Gitlab::Git::CommandError) end end @@ -173,7 +153,7 @@ describe Gitlab::Git::Repository, seed_helper: true do end it 'returns UTF-8' do - expect(subject.first.encoding).to eq(Encoding.find('UTF-8')) + expect(subject.first).to be_utf8 end describe '#last' do @@ -183,31 +163,21 @@ describe Gitlab::Git::Repository, seed_helper: true do it { is_expected.to include("v1.0.0") } it { is_expected.not_to include("v5.0.0") } - context 'with gitaly enabled' do - before do - stub_gitaly - end - - after do - Gitlab::GitalyClient.clear_stubs! - end - - it 'gets the tag names from GitalyClient' do - expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:tag_names) - subject - end + it 'gets the tag names from GitalyClient' do + expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:tag_names) + subject + end - it 'wraps GRPC not found' do - expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:tag_names) - .and_raise(GRPC::NotFound) - expect { subject }.to raise_error(Gitlab::Git::Repository::NoRepository) - end + it 'wraps GRPC not found' do + expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:tag_names) + .and_raise(GRPC::NotFound) + expect { subject }.to raise_error(Gitlab::Git::Repository::NoRepository) + end - it 'wraps GRPC exceptions' do - expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:tag_names) - .and_raise(GRPC::Unknown) - expect { subject }.to raise_error(Gitlab::Git::CommandError) - end + it 'wraps GRPC exceptions' do + expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:tag_names) + .and_raise(GRPC::Unknown) + expect { subject }.to raise_error(Gitlab::Git::CommandError) end end @@ -358,6 +328,38 @@ describe Gitlab::Git::Repository, seed_helper: true do end end + describe '#submodule_url_for' do + let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH) } + let(:ref) { 'master' } + + def submodule_url(path) + repository.submodule_url_for(ref, path) + end + + it { expect(submodule_url('six')).to eq('git://github.com/randx/six.git') } + it { expect(submodule_url('nested/six')).to eq('git://github.com/randx/six.git') } + it { expect(submodule_url('deeper/nested/six')).to eq('git://github.com/randx/six.git') } + it { expect(submodule_url('invalid/path')).to eq(nil) } + + context 'uncommitted submodule dir' do + let(:ref) { 'fix-existing-submodule-dir' } + + it { expect(submodule_url('submodule-existing-dir')).to eq(nil) } + end + + context 'tags' do + let(:ref) { 'v1.2.1' } + + it { expect(submodule_url('six')).to eq('git://github.com/randx/six.git') } + end + + context 'no submodules at commit' do + let(:ref) { '6d39438' } + + it { expect(submodule_url('six')).to eq(nil) } + end + end + context '#submodules' do let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH) } @@ -1281,42 +1283,31 @@ describe Gitlab::Git::Repository, seed_helper: true do expect(@repo.local_branches.any? { |branch| branch.name == 'local_branch' }).to eq(true) end - context 'with gitaly enabled' do - before do - stub_gitaly - end - - after do - Gitlab::GitalyClient.clear_stubs! - end - - it 'returns a Branch with UTF-8 fields' do - branches = @repo.local_branches.to_a - expect(branches.size).to be > 0 - utf_8 = Encoding.find('utf-8') - branches.each do |branch| - expect(branch.name.encoding).to eq(utf_8) - expect(branch.target.encoding).to eq(utf_8) unless branch.target.nil? - end + it 'returns a Branch with UTF-8 fields' do + branches = @repo.local_branches.to_a + expect(branches.size).to be > 0 + branches.each do |branch| + expect(branch.name).to be_utf8 + expect(branch.target).to be_utf8 unless branch.target.nil? end + end - it 'gets the branches from GitalyClient' do - expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:local_branches) - .and_return([]) - @repo.local_branches - end + it 'gets the branches from GitalyClient' do + expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:local_branches) + .and_return([]) + @repo.local_branches + end - it 'wraps GRPC not found' do - expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:local_branches) - .and_raise(GRPC::NotFound) - expect { @repo.local_branches }.to raise_error(Gitlab::Git::Repository::NoRepository) - end + it 'wraps GRPC not found' do + expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:local_branches) + .and_raise(GRPC::NotFound) + expect { @repo.local_branches }.to raise_error(Gitlab::Git::Repository::NoRepository) + end - it 'wraps GRPC exceptions' do - expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:local_branches) - .and_raise(GRPC::Unknown) - expect { @repo.local_branches }.to raise_error(Gitlab::Git::CommandError) - end + it 'wraps GRPC exceptions' do + expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:local_branches) + .and_raise(GRPC::Unknown) + expect { @repo.local_branches }.to raise_error(Gitlab::Git::CommandError) end end @@ -1395,11 +1386,4 @@ describe Gitlab::Git::Repository, seed_helper: true do sha = Rugged::Commit.create(repo, options) repo.lookup(sha) end - - def stub_gitaly - allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_return(true) - - stub = double(:stub) - allow(Gitaly::Ref::Stub).to receive(:new).and_return(stub) - end end diff --git a/spec/lib/gitlab/gitaly_client/ref_spec.rb b/spec/lib/gitlab/gitaly_client/ref_spec.rb index 8ad39a02b93..df22fcad902 100644 --- a/spec/lib/gitlab/gitaly_client/ref_spec.rb +++ b/spec/lib/gitlab/gitaly_client/ref_spec.rb @@ -6,17 +6,6 @@ describe Gitlab::GitalyClient::Ref do let(:relative_path) { project.path_with_namespace + '.git' } let(:client) { described_class.new(project.repository) } - before do - allow(Gitlab.config.gitaly).to receive(:enabled).and_return(true) - end - - after do - # When we say `expect_any_instance_of(Gitaly::Ref::Stub)` a double is created, - # and because GitalyClient shares stubs these will get passed from example to - # example, which will cause an error, so we clean the stubs after each example. - Gitlab::GitalyClient.clear_stubs! - end - describe '#branch_names' do it 'sends a find_all_branch_names message' do expect_any_instance_of(Gitaly::Ref::Stub) @@ -82,4 +71,13 @@ describe Gitlab::GitalyClient::Ref do expect { client.local_branches(sort_by: 'invalid_sort') }.to raise_error(ArgumentError) end end + + describe '#find_ref_name', seed_helper: true do + let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH) } + let(:client) { described_class.new(repository) } + subject { client.find_ref_name(SeedRepo::Commit::ID, 'refs/heads/master') } + + it { should be_utf8 } + it { should eq('refs/heads/master') } + end end diff --git a/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb b/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb index 61c10d47434..b333e162909 100644 --- a/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb +++ b/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb @@ -97,30 +97,40 @@ describe Gitlab::HealthChecks::FsShardsCheck do }.with_indifferent_access end - it { is_expected.to all(have_attributes(labels: { shard: :default })) } + # Unsolved intermittent failure in CI https://gitlab.com/gitlab-org/gitlab-ce/issues/31128 + around(:each) do |example| # rubocop:disable RSpec/AroundBlock + times_to_try = ENV['CI'] ? 4 : 1 + example.run_with_retry retry: times_to_try + end - it { is_expected.to include(an_object_having_attributes(name: :filesystem_accessible, value: 0)) } - it { is_expected.to include(an_object_having_attributes(name: :filesystem_readable, value: 0)) } - it { is_expected.to include(an_object_having_attributes(name: :filesystem_writable, value: 0)) } + it 'provides metrics' do + expect(subject).to all(have_attributes(labels: { shard: :default })) + expect(subject).to include(an_object_having_attributes(name: :filesystem_accessible, value: 0)) + expect(subject).to include(an_object_having_attributes(name: :filesystem_readable, value: 0)) + expect(subject).to include(an_object_having_attributes(name: :filesystem_writable, value: 0)) - it { is_expected.to include(an_object_having_attributes(name: :filesystem_access_latency, value: be >= 0)) } - it { is_expected.to include(an_object_having_attributes(name: :filesystem_read_latency, value: be >= 0)) } - it { is_expected.to include(an_object_having_attributes(name: :filesystem_write_latency, value: be >= 0)) } + expect(subject).to include(an_object_having_attributes(name: :filesystem_access_latency, value: be >= 0)) + expect(subject).to include(an_object_having_attributes(name: :filesystem_read_latency, value: be >= 0)) + expect(subject).to include(an_object_having_attributes(name: :filesystem_write_latency, value: be >= 0)) + end end context 'storage points to directory that has both read and write rights' do before do FileUtils.chmod_R(0755, tmp_dir) end - it { is_expected.to all(have_attributes(labels: { shard: :default })) } - it { is_expected.to include(an_object_having_attributes(name: :filesystem_accessible, value: 1)) } - it { is_expected.to include(an_object_having_attributes(name: :filesystem_readable, value: 1)) } - it { is_expected.to include(an_object_having_attributes(name: :filesystem_writable, value: 1)) } + it 'provides metrics' do + expect(subject).to all(have_attributes(labels: { shard: :default })) - it { is_expected.to include(an_object_having_attributes(name: :filesystem_access_latency, value: be >= 0)) } - it { is_expected.to include(an_object_having_attributes(name: :filesystem_read_latency, value: be >= 0)) } - it { is_expected.to include(an_object_having_attributes(name: :filesystem_write_latency, value: be >= 0)) } + expect(subject).to include(an_object_having_attributes(name: :filesystem_accessible, value: 1)) + expect(subject).to include(an_object_having_attributes(name: :filesystem_readable, value: 1)) + expect(subject).to include(an_object_having_attributes(name: :filesystem_writable, value: 1)) + + expect(subject).to include(an_object_having_attributes(name: :filesystem_access_latency, value: be >= 0)) + expect(subject).to include(an_object_having_attributes(name: :filesystem_read_latency, value: be >= 0)) + expect(subject).to include(an_object_having_attributes(name: :filesystem_write_latency, value: be >= 0)) + end end end end diff --git a/spec/lib/gitlab/metrics/connection_rack_middleware_spec.rb b/spec/lib/gitlab/metrics/connection_rack_middleware_spec.rb new file mode 100644 index 00000000000..94251af305f --- /dev/null +++ b/spec/lib/gitlab/metrics/connection_rack_middleware_spec.rb @@ -0,0 +1,88 @@ +require 'spec_helper' + +describe Gitlab::Metrics::ConnectionRackMiddleware do + let(:app) { double('app') } + subject { described_class.new(app) } + + around do |example| + Timecop.freeze { example.run } + end + + describe '#call' do + let(:status) { 100 } + let(:env) { { 'REQUEST_METHOD' => 'GET' } } + let(:stack_result) { [status, {}, 'body'] } + + before do + allow(app).to receive(:call).and_return(stack_result) + end + + context '@app.call succeeds with 200' do + before do + allow(app).to receive(:call).and_return([200, nil, nil]) + end + + it 'increments response count with status label' do + expect(described_class).to receive_message_chain(:rack_response_count, :increment).with(include(status: 200, method: 'get')) + + subject.call(env) + end + + it 'increments requests count' do + expect(described_class).to receive_message_chain(:rack_request_count, :increment).with(method: 'get') + + subject.call(env) + end + + it 'measures execution time' do + execution_time = 10 + allow(app).to receive(:call) do |*args| + Timecop.freeze(execution_time.seconds) + end + + expect(described_class).to receive_message_chain(:rack_execution_time, :observe).with({}, execution_time) + + subject.call(env) + end + end + + context '@app.call throws exception' do + let(:rack_response_count) { double('rack_response_count') } + + before do + allow(app).to receive(:call).and_raise(StandardError) + allow(described_class).to receive(:rack_response_count).and_return(rack_response_count) + end + + it 'increments exceptions count' do + expect(described_class).to receive_message_chain(:rack_uncaught_errors_count, :increment) + + expect { subject.call(env) }.to raise_error(StandardError) + end + + it 'increments requests count' do + expect(described_class).to receive_message_chain(:rack_request_count, :increment).with(method: 'get') + + expect { subject.call(env) }.to raise_error(StandardError) + end + + it "does't increment response count" do + expect(described_class.rack_response_count).not_to receive(:increment) + + expect { subject.call(env) }.to raise_error(StandardError) + end + + it 'measures execution time' do + execution_time = 10 + allow(app).to receive(:call) do |*args| + Timecop.freeze(execution_time.seconds) + raise StandardError + end + + expect(described_class).to receive_message_chain(:rack_execution_time, :observe).with({}, execution_time) + + expect { subject.call(env) }.to raise_error(StandardError) + end + end + end +end diff --git a/spec/lib/gitlab/metrics/sampler_spec.rb b/spec/lib/gitlab/metrics/influx_sampler_spec.rb index d07ce6f81af..0bc68d64276 100644 --- a/spec/lib/gitlab/metrics/sampler_spec.rb +++ b/spec/lib/gitlab/metrics/influx_sampler_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::Metrics::Sampler do +describe Gitlab::Metrics::InfluxSampler do let(:sampler) { described_class.new(5) } after do @@ -8,10 +8,10 @@ describe Gitlab::Metrics::Sampler do end describe '#start' do - it 'gathers a sample at a given interval' do - expect(sampler).to receive(:sleep).with(a_kind_of(Numeric)) - expect(sampler).to receive(:sample) - expect(sampler).to receive(:loop).and_yield + it 'runs once and gathers a sample at a given interval' do + expect(sampler).to receive(:sleep).with(a_kind_of(Numeric)).twice + expect(sampler).to receive(:sample).once + expect(sampler).to receive(:running).and_return(false, true, false) sampler.start.join end diff --git a/spec/lib/gitlab/metrics/unicorn_sampler_spec.rb b/spec/lib/gitlab/metrics/unicorn_sampler_spec.rb new file mode 100644 index 00000000000..dc0d1f2e940 --- /dev/null +++ b/spec/lib/gitlab/metrics/unicorn_sampler_spec.rb @@ -0,0 +1,108 @@ +require 'spec_helper' + +describe Gitlab::Metrics::UnicornSampler do + subject { described_class.new(1.second) } + + describe '#sample' do + let(:unicorn) { double('unicorn') } + let(:raindrops) { double('raindrops') } + let(:stats) { double('stats') } + + before do + stub_const('Unicorn', unicorn) + stub_const('Raindrops::Linux', raindrops) + allow(raindrops).to receive(:unix_listener_stats).and_return({}) + allow(raindrops).to receive(:tcp_listener_stats).and_return({}) + end + + context 'unicorn listens on unix sockets' do + let(:socket_address) { '/some/sock' } + let(:sockets) { [socket_address] } + + before do + allow(unicorn).to receive(:listener_names).and_return(sockets) + end + + it 'samples socket data' do + expect(raindrops).to receive(:unix_listener_stats).with(sockets) + + subject.sample + end + + context 'stats collected' do + before do + allow(stats).to receive(:active).and_return('active') + allow(stats).to receive(:queued).and_return('queued') + allow(raindrops).to receive(:unix_listener_stats).and_return({ socket_address => stats }) + end + + it 'updates metrics type unix and with addr' do + labels = { type: 'unix', address: socket_address } + + expect(subject).to receive_message_chain(:unicorn_active_connections, :set).with(labels, 'active') + expect(subject).to receive_message_chain(:unicorn_queued_connections, :set).with(labels, 'queued') + + subject.sample + end + end + end + + context 'unicorn listens on tcp sockets' do + let(:tcp_socket_address) { '0.0.0.0:8080' } + let(:tcp_sockets) { [tcp_socket_address] } + + before do + allow(unicorn).to receive(:listener_names).and_return(tcp_sockets) + end + + it 'samples socket data' do + expect(raindrops).to receive(:tcp_listener_stats).with(tcp_sockets) + + subject.sample + end + + context 'stats collected' do + before do + allow(stats).to receive(:active).and_return('active') + allow(stats).to receive(:queued).and_return('queued') + allow(raindrops).to receive(:tcp_listener_stats).and_return({ tcp_socket_address => stats }) + end + + it 'updates metrics type unix and with addr' do + labels = { type: 'tcp', address: tcp_socket_address } + + expect(subject).to receive_message_chain(:unicorn_active_connections, :set).with(labels, 'active') + expect(subject).to receive_message_chain(:unicorn_queued_connections, :set).with(labels, 'queued') + + subject.sample + end + end + end + end + + describe '#start' do + context 'when enabled' do + before do + allow(subject).to receive(:enabled?).and_return(true) + end + + it 'creates new thread' do + expect(Thread).to receive(:new) + + subject.start + end + end + + context 'when disabled' do + before do + allow(subject).to receive(:enabled?).and_return(false) + end + + it "doesn't create new thread" do + expect(Thread).not_to receive(:new) + + subject.start + end + end + end +end diff --git a/spec/lib/gitlab/popen_spec.rb b/spec/lib/gitlab/popen_spec.rb index 4ae216d55b0..af50ecdb2ab 100644 --- a/spec/lib/gitlab/popen_spec.rb +++ b/spec/lib/gitlab/popen_spec.rb @@ -32,6 +32,17 @@ describe 'Gitlab::Popen', lib: true, no_db: true do end end + context 'with custom options' do + let(:vars) { { 'foobar' => 123, 'PWD' => path } } + let(:options) { { chdir: path } } + + it 'calls popen3 with the provided environment variables' do + expect(Open3).to receive(:popen3).with(vars, 'ls', options) + + @output, @status = @klass.new.popen(%w(ls), path, { 'foobar' => 123 }) + end + end + context 'without a directory argument' do before do @output, @status = @klass.new.popen(%w(ls)) @@ -45,7 +56,7 @@ describe 'Gitlab::Popen', lib: true, no_db: true do before do @output, @status = @klass.new.popen(%w[cat]) { |stdin| stdin.write 'hello' } end - + it { expect(@status).to be_zero } it { expect(@output).to eq('hello') } end diff --git a/spec/lib/gitlab/shell_spec.rb b/spec/lib/gitlab/shell_spec.rb index a97a0f8452b..5b1b8f9516a 100644 --- a/spec/lib/gitlab/shell_spec.rb +++ b/spec/lib/gitlab/shell_spec.rb @@ -4,6 +4,7 @@ require 'stringio' describe Gitlab::Shell, lib: true do let(:project) { double('Project', id: 7, path: 'diaspora') } let(:gitlab_shell) { Gitlab::Shell.new } + let(:popen_vars) { { 'GIT_TERMINAL_PROMPT' => ENV['GIT_TERMINAL_PROMPT'] } } before do allow(Project).to receive(:find).and_return(project) @@ -50,7 +51,7 @@ describe Gitlab::Shell, lib: true do describe '#add_key' do it 'removes trailing garbage' do allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path) - expect(Gitlab::Utils).to receive(:system_silent).with( + expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with( [:gitlab_shell_keys_path, 'add-key', 'key-123', 'ssh-rsa foobar'] ) @@ -100,17 +101,91 @@ describe Gitlab::Shell, lib: true do allow(Gitlab.config.gitlab_shell).to receive(:git_timeout).and_return(800) end + describe '#add_repository' do + it 'returns true when the command succeeds' do + expect(Gitlab::Popen).to receive(:popen) + .with([projects_path, 'add-project', 'current/storage', 'project/path.git'], + nil, popen_vars).and_return([nil, 0]) + + expect(gitlab_shell.add_repository('current/storage', 'project/path')).to be true + end + + it 'returns false when the command fails' do + expect(Gitlab::Popen).to receive(:popen) + .with([projects_path, 'add-project', 'current/storage', 'project/path.git'], + nil, popen_vars).and_return(["error", 1]) + + expect(gitlab_shell.add_repository('current/storage', 'project/path')).to be false + end + end + + describe '#remove_repository' do + it 'returns true when the command succeeds' do + expect(Gitlab::Popen).to receive(:popen) + .with([projects_path, 'rm-project', 'current/storage', 'project/path.git'], + nil, popen_vars).and_return([nil, 0]) + + expect(gitlab_shell.remove_repository('current/storage', 'project/path')).to be true + end + + it 'returns false when the command fails' do + expect(Gitlab::Popen).to receive(:popen) + .with([projects_path, 'rm-project', 'current/storage', 'project/path.git'], + nil, popen_vars).and_return(["error", 1]) + + expect(gitlab_shell.remove_repository('current/storage', 'project/path')).to be false + end + end + + describe '#mv_repository' do + it 'returns true when the command succeeds' do + expect(Gitlab::Popen).to receive(:popen) + .with([projects_path, 'mv-project', 'current/storage', 'project/path.git', 'project/newpath.git'], + nil, popen_vars).and_return([nil, 0]) + + expect(gitlab_shell.mv_repository('current/storage', 'project/path', 'project/newpath')).to be true + end + + it 'returns false when the command fails' do + expect(Gitlab::Popen).to receive(:popen) + .with([projects_path, 'mv-project', 'current/storage', 'project/path.git', 'project/newpath.git'], + nil, popen_vars).and_return(["error", 1]) + + expect(gitlab_shell.mv_repository('current/storage', 'project/path', 'project/newpath')).to be false + end + end + + describe '#fork_repository' do + it 'returns true when the command succeeds' do + expect(Gitlab::Popen).to receive(:popen) + .with([projects_path, 'fork-project', 'current/storage', 'project/path.git', 'new/storage', 'new-namespace'], + nil, popen_vars).and_return([nil, 0]) + + expect(gitlab_shell.fork_repository('current/storage', 'project/path', 'new/storage', 'new-namespace')).to be true + end + + it 'return false when the command fails' do + expect(Gitlab::Popen).to receive(:popen) + .with([projects_path, 'fork-project', 'current/storage', 'project/path.git', 'new/storage', 'new-namespace'], + nil, popen_vars).and_return(["error", 1]) + + expect(gitlab_shell.fork_repository('current/storage', 'project/path', 'new/storage', 'new-namespace')).to be false + end + end + describe '#fetch_remote' do it 'returns true when the command succeeds' do expect(Gitlab::Popen).to receive(:popen) - .with([projects_path, 'fetch-remote', 'current/storage', 'project/path.git', 'new/storage', '800']).and_return([nil, 0]) + .with([projects_path, 'fetch-remote', 'current/storage', 'project/path.git', 'new/storage', '800'], + nil, popen_vars).and_return([nil, 0]) expect(gitlab_shell.fetch_remote('current/storage', 'project/path', 'new/storage')).to be true end it 'raises an exception when the command fails' do expect(Gitlab::Popen).to receive(:popen) - .with([projects_path, 'fetch-remote', 'current/storage', 'project/path.git', 'new/storage', '800']).and_return(["error", 1]) + .with([projects_path, 'fetch-remote', 'current/storage', 'project/path.git', 'new/storage', '800'], + nil, popen_vars).and_return(["error", 1]) expect { gitlab_shell.fetch_remote('current/storage', 'project/path', 'new/storage') }.to raise_error(Gitlab::Shell::Error, "error") end @@ -119,14 +194,16 @@ describe Gitlab::Shell, lib: true do describe '#import_repository' do it 'returns true when the command succeeds' do expect(Gitlab::Popen).to receive(:popen) - .with([projects_path, 'import-project', 'current/storage', 'project/path.git', 'https://gitlab.com/gitlab-org/gitlab-ce.git', "800"]).and_return([nil, 0]) + .with([projects_path, 'import-project', 'current/storage', 'project/path.git', 'https://gitlab.com/gitlab-org/gitlab-ce.git', "800"], + nil, popen_vars).and_return([nil, 0]) expect(gitlab_shell.import_repository('current/storage', 'project/path', 'https://gitlab.com/gitlab-org/gitlab-ce.git')).to be true end it 'raises an exception when the command fails' do expect(Gitlab::Popen).to receive(:popen) - .with([projects_path, 'import-project', 'current/storage', 'project/path.git', 'https://gitlab.com/gitlab-org/gitlab-ce.git', "800"]).and_return(["error", 1]) + .with([projects_path, 'import-project', 'current/storage', 'project/path.git', 'https://gitlab.com/gitlab-org/gitlab-ce.git', "800"], + nil, popen_vars).and_return(["error", 1]) expect { gitlab_shell.import_repository('current/storage', 'project/path', 'https://gitlab.com/gitlab-org/gitlab-ce.git') }.to raise_error(Gitlab::Shell::Error, "error") end diff --git a/spec/migrations/rename_duplicated_variable_key_spec.rb b/spec/migrations/rename_duplicated_variable_key_spec.rb new file mode 100644 index 00000000000..11096564dfa --- /dev/null +++ b/spec/migrations/rename_duplicated_variable_key_spec.rb @@ -0,0 +1,34 @@ +require 'spec_helper' +require Rails.root.join('db', 'migrate', '20170622135451_rename_duplicated_variable_key.rb') + +describe RenameDuplicatedVariableKey, :migration do + let(:variables) { table(:ci_variables) } + let(:projects) { table(:projects) } + + before do + projects.create!(id: 1) + variables.create!(id: 1, key: 'key1', project_id: 1) + variables.create!(id: 2, key: 'key2', project_id: 1) + variables.create!(id: 3, key: 'keyX', project_id: 1) + variables.create!(id: 4, key: 'keyX', project_id: 1) + variables.create!(id: 5, key: 'keyY', project_id: 1) + variables.create!(id: 6, key: 'keyX', project_id: 1) + variables.create!(id: 7, key: 'key7', project_id: 1) + variables.create!(id: 8, key: 'keyY', project_id: 1) + end + + it 'correctly remove duplicated records with smaller id' do + migrate! + + expect(variables.pluck(:id, :key)).to contain_exactly( + [1, 'key1'], + [2, 'key2'], + [3, 'keyX_3'], + [4, 'keyX_4'], + [5, 'keyY_5'], + [6, 'keyX'], + [7, 'key7'], + [8, 'keyY'] + ) + end +end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index f955d9c81b8..a7ba3a7c43e 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -998,13 +998,17 @@ describe Ci::Build, :models do describe '#ref_slug' do { - 'master' => 'master', - '1-foo' => '1-foo', - 'fix/1-foo' => 'fix-1-foo', - 'fix-1-foo' => 'fix-1-foo', - 'a' * 63 => 'a' * 63, - 'a' * 64 => 'a' * 63, - 'FOO' => 'foo' + 'master' => 'master', + '1-foo' => '1-foo', + 'fix/1-foo' => 'fix-1-foo', + 'fix-1-foo' => 'fix-1-foo', + 'a' * 63 => 'a' * 63, + 'a' * 64 => 'a' * 63, + 'FOO' => 'foo', + '-' + 'a' * 61 + '-' => 'a' * 61, + '-' + 'a' * 62 + '-' => 'a' * 62, + '-' + 'a' * 63 + '-' => 'a' * 62, + 'a' * 62 + ' ' => 'a' * 62 }.each do |ref, slug| it "transforms #{ref} to #{slug}" do build.ref = ref diff --git a/spec/models/ci/variable_spec.rb b/spec/models/ci/variable_spec.rb index 329682a0771..50f7c029af8 100644 --- a/spec/models/ci/variable_spec.rb +++ b/spec/models/ci/variable_spec.rb @@ -3,8 +3,16 @@ require 'spec_helper' describe Ci::Variable, models: true do subject { build(:ci_variable) } - it { is_expected.to include_module(HasVariable) } - it { is_expected.to validate_uniqueness_of(:key).scoped_to(:project_id) } + let(:secret_value) { 'secret' } + + describe 'validations' do + it { is_expected.to include_module(HasVariable) } + it { is_expected.to validate_uniqueness_of(:key).scoped_to(:project_id, :environment_scope) } + it { is_expected.to validate_length_of(:key).is_at_most(255) } + it { is_expected.to allow_value('foo').for(:key) } + it { is_expected.not_to allow_value('foo bar').for(:key) } + it { is_expected.not_to allow_value('foo/bar').for(:key) } + end describe '.unprotected' do subject { described_class.unprotected } diff --git a/spec/models/concerns/routable_spec.rb b/spec/models/concerns/routable_spec.rb index 65f05121b40..36aedd2f701 100644 --- a/spec/models/concerns/routable_spec.rb +++ b/spec/models/concerns/routable_spec.rb @@ -132,6 +132,19 @@ describe Group, 'Routable' do end end + describe '#expires_full_path_cache' do + context 'with RequestStore active', :request_store do + it 'expires the full_path cache' do + expect(group.full_path).to eq('foo') + + group.route.update(path: 'bar', name: 'bar') + group.expires_full_path_cache + + expect(group.full_path).to eq('bar') + end + end + end + describe '#full_name' do let(:group) { create(:group) } let(:nested_group) { create(:group, parent: group) } diff --git a/spec/models/concerns/sortable_spec.rb b/spec/models/concerns/sortable_spec.rb new file mode 100644 index 00000000000..d1e17c4f684 --- /dev/null +++ b/spec/models/concerns/sortable_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe Sortable do + let(:relation) { Issue.all } + + describe '#where' do + it 'orders by id, descending' do + order_node = relation.where(iid: 1).order_values.first + expect(order_node).to be_a(Arel::Nodes::Descending) + expect(order_node.expr.name).to eq(:id) + end + end + + describe '#find_by' do + it 'does not order' do + expect(relation).to receive(:unscope).with(:order).and_call_original + + relation.find_by(iid: 1) + end + end +end diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb index b0635c6a90a..0a2cd8c2957 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -120,28 +120,17 @@ describe Environment, models: true do let(:head_commit) { project.commit } let(:commit) { project.commit.parent } - context 'Gitaly find_ref_name feature disabled' do - it 'returns deployment id for the environment' do - expect(environment.first_deployment_for(commit)).to eq deployment1 - end + it 'returns deployment id for the environment' do + expect(environment.first_deployment_for(commit)).to eq deployment1 + end - it 'return nil when no deployment is found' do - expect(environment.first_deployment_for(head_commit)).to eq nil - end + it 'return nil when no deployment is found' do + expect(environment.first_deployment_for(head_commit)).to eq nil end - # TODO: Uncomment when feature is reenabled - # context 'Gitaly find_ref_name feature enabled' do - # before do - # allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(:find_ref_name).and_return(true) - # end - # - # it 'calls GitalyClient' do - # expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:find_ref_name) - # - # environment.first_deployment_for(commit) - # end - # end + it 'returns a UTF-8 ref' do + expect(environment.first_deployment_for(commit).ref).to be_utf8 + end end describe '#environment_type' do diff --git a/spec/models/forked_project_link_spec.rb b/spec/models/forked_project_link_spec.rb index 6e8d43f988c..5c13cf584f9 100644 --- a/spec/models/forked_project_link_spec.rb +++ b/spec/models/forked_project_link_spec.rb @@ -2,53 +2,75 @@ require 'spec_helper' describe ForkedProjectLink, "add link on fork" do let(:project_from) { create(:project, :repository) } + let(:project_to) { fork_project(project_from, user) } let(:user) { create(:user) } let(:namespace) { user.namespace } before do - create(:project_member, :reporter, user: user, project: project_from) - @project_to = fork_project(project_from, user) + project_from.add_reporter(user) + end + + it 'project_from knows its forks' do + _ = project_to + + expect(project_from.forks.count).to eq(1) end it "project_to knows it is forked" do - expect(@project_to.forked?).to be_truthy + expect(project_to.forked?).to be_truthy end it "project knows who it is forked from" do - expect(@project_to.forked_from_project).to eq(project_from) + expect(project_to.forked_from_project).to eq(project_from) end -end -describe '#forked?' do - let(:forked_project_link) { build(:forked_project_link) } - let(:project_from) { create(:project, :repository) } - let(:project_to) { create(:project, forked_project_link: forked_project_link) } + context 'project_to is pending_delete' do + before do + project_to.update!(pending_delete: true) + end - before :each do - forked_project_link.forked_from_project = project_from - forked_project_link.forked_to_project = project_to - forked_project_link.save! + it { expect(project_from.forks.count).to eq(0) } end - it "project_to knows it is forked" do - expect(project_to.forked?).to be_truthy - end + context 'project_from is pending_delete' do + before do + project_from.update!(pending_delete: true) + end - it "project_from is not forked" do - expect(project_from.forked?).to be_falsey + it { expect(project_to.forked_from_project).to be_nil } end - it "project_to.destroy destroys fork_link" do - expect(forked_project_link).to receive(:destroy) - project_to.destroy + describe '#forked?' do + let(:project_to) { create(:project, forked_project_link: forked_project_link) } + let(:forked_project_link) { build(:forked_project_link) } + + before do + forked_project_link.forked_from_project = project_from + forked_project_link.forked_to_project = project_to + forked_project_link.save! + end + + it "project_to knows it is forked" do + expect(project_to.forked?).to be_truthy + end + + it "project_from is not forked" do + expect(project_from.forked?).to be_falsey + end + + it "project_to.destroy destroys fork_link" do + expect(forked_project_link).to receive(:destroy) + + project_to.destroy + end end -end -def fork_project(from_project, user) - shell = double('gitlab_shell', fork_repository: true) + def fork_project(from_project, user) + service = Projects::ForkService.new(from_project, user) + shell = double('gitlab_shell', fork_repository: true) - service = Projects::ForkService.new(from_project, user) - allow(service).to receive(:gitlab_shell).and_return(shell) + allow(service).to receive(:gitlab_shell).and_return(shell) - service.execute + service.execute + end end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index bb5273074a2..587d4b83cb4 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -105,6 +105,22 @@ describe MergeRequest, models: true do end end + describe '#assignee_ids' do + it 'returns an array of the assigned user id' do + subject.assignee_id = 123 + + expect(subject.assignee_ids).to eq([123]) + end + end + + describe '#assignee_ids=' do + it 'sets assignee_id to the last id in the array' do + subject.assignee_ids = [123, 456] + + expect(subject.assignee_id).to eq(456) + end + end + describe '#assignee_or_author?' do let(:user) { create(:user) } diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 6197a390da8..ae16b497c2f 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -1220,6 +1220,8 @@ describe Project, models: true do expect(project).to receive(:expire_caches_before_rename) + expect(project).to receive(:expires_full_path_cache) + project.rename_repo end @@ -1348,7 +1350,7 @@ describe Project, models: true do .with(project.repository_storage_path, project.path_with_namespace) .and_return(true) - expect(project).to receive(:create_repository) + expect(project).to receive(:create_repository).with(force: true) project.ensure_repository end @@ -1361,6 +1363,19 @@ describe Project, models: true do project.ensure_repository end + + it 'creates the repository if it is a fork' do + expect(project).to receive(:forked?).and_return(true) + + allow(project).to receive(:repository_exists?) + .and_return(false) + + expect(shell).to receive(:add_repository) + .with(project.repository_storage_path, project.path_with_namespace) + .and_return(true) + + project.ensure_repository + end end describe '#user_can_push_to_empty_repo?' do diff --git a/spec/policies/global_policy_spec.rb b/spec/policies/global_policy_spec.rb new file mode 100644 index 00000000000..bb0fa0c0e9c --- /dev/null +++ b/spec/policies/global_policy_spec.rb @@ -0,0 +1,34 @@ +require 'spec_helper' + +describe GlobalPolicy, models: true do + let(:current_user) { create(:user) } + let(:user) { create(:user) } + + subject { GlobalPolicy.new(current_user, [user]) } + + describe "reading the list of users" do + context "for a logged in user" do + it { is_expected.to be_allowed(:read_users_list) } + end + + context "for an anonymous user" do + let(:current_user) { nil } + + context "when the public level is restricted" do + before do + stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC]) + end + + it { is_expected.not_to be_allowed(:read_users_list) } + end + + context "when the public level is not restricted" do + before do + stub_application_setting(restricted_visibility_levels: []) + end + + it { is_expected.to be_allowed(:read_users_list) } + end + end + end +end diff --git a/spec/requests/api/commit_statuses_spec.rb b/spec/requests/api/commit_statuses_spec.rb index cdb60fc0d1a..8b62aa268d9 100644 --- a/spec/requests/api/commit_statuses_spec.rb +++ b/spec/requests/api/commit_statuses_spec.rb @@ -237,6 +237,28 @@ describe API::CommitStatuses do end end + context 'when retrying a commit status' do + before do + post api(post_url, developer), + { state: 'failed', name: 'test', ref: 'master' } + + post api(post_url, developer), + { state: 'success', name: 'test', ref: 'master' } + end + + it 'correctly posts a new commit status' do + expect(response).to have_http_status(201) + expect(json_response['sha']).to eq(commit.id) + expect(json_response['status']).to eq('success') + end + + it 'retries a commit status' do + expect(CommitStatus.count).to eq 2 + expect(CommitStatus.first).to be_retried + expect(CommitStatus.last.pipeline).to be_success + end + end + context 'when status is invalid' do before do post api(post_url, developer), state: 'invalid' diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb index 191c60aba31..25ec44fa036 100644 --- a/spec/requests/api/helpers_spec.rb +++ b/spec/requests/api/helpers_spec.rb @@ -14,6 +14,10 @@ describe API::Helpers do let(:request) { Rack::Request.new(env) } let(:header) { } + before do + allow_any_instance_of(self.class).to receive(:options).and_return({}) + end + def set_env(user_or_token, identifier) clear_env clear_param @@ -167,7 +171,6 @@ describe API::Helpers do it "returns nil for a token without the appropriate scope" do personal_access_token = create(:personal_access_token, user: user, scopes: ['read_user']) env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token - allow_access_with_scope('write_user') expect(current_user).to be_nil end diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index c0174b304c8..70b94a09e6b 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -13,9 +13,40 @@ describe API::Users do describe 'GET /users' do context "when unauthenticated" do - it "returns authentication error" do + it "returns authorization error when the `username` parameter is not passed" do get api("/users") - expect(response).to have_http_status(401) + + expect(response).to have_http_status(403) + end + + it "returns the user when a valid `username` parameter is passed" do + user = create(:user) + + get api("/users"), username: user.username + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.size).to eq(1) + expect(json_response[0]['id']).to eq(user.id) + expect(json_response[0]['username']).to eq(user.username) + end + + it "returns authorization error when the `username` parameter refers to an inaccessible user" do + user = create(:user) + + stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC]) + + get api("/users"), username: user.username + + expect(response).to have_http_status(403) + end + + it "returns an empty response when an invalid `username` parameter is passed" do + get api("/users"), username: 'invalid' + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.size).to eq(0) end end @@ -138,6 +169,7 @@ describe API::Users do describe "GET /users/:id" do it "returns a user by id" do get api("/users/#{user.id}", user) + expect(response).to have_http_status(200) expect(json_response['username']).to eq(user.username) end @@ -148,9 +180,22 @@ describe API::Users do expect(json_response['is_admin']).to be_nil end - it "returns a 401 if unauthenticated" do - get api("/users/9998") - expect(response).to have_http_status(401) + context 'for an anonymous user' do + it "returns a user by id" do + get api("/users/#{user.id}") + + expect(response).to have_http_status(200) + expect(json_response['username']).to eq(user.username) + end + + it "returns a 404 if the target user is present but inaccessible" do + allow(Ability).to receive(:allowed?).and_call_original + allow(Ability).to receive(:allowed?).with(nil, :read_user, user).and_return(false) + + get api("/users/#{user.id}") + + expect(response).to have_http_status(404) + end end it "returns a 404 error if user id not found" do @@ -345,6 +390,14 @@ describe API::Users do expect(json_response['identities'].first['provider']).to eq('github') end end + + context "scopes" do + let(:user) { admin } + let(:path) { '/users' } + let(:api_call) { method(:api) } + + include_examples 'does not allow the "read_user" scope' + end end describe "GET /users/sign_up" do @@ -842,6 +895,13 @@ describe API::Users do expect(response).to match_response_schema('public_api/v4/user/public') expect(json_response['id']).to eq(user.id) end + + context "scopes" do + let(:path) { "/user" } + let(:api_call) { method(:api) } + + include_examples 'allows the "read_user" scope' + end end context 'with admin' do @@ -911,6 +971,13 @@ describe API::Users do expect(json_response).to be_an Array expect(json_response.first["title"]).to eq(key.title) end + + context "scopes" do + let(:path) { "/user/keys" } + let(:api_call) { method(:api) } + + include_examples 'allows the "read_user" scope' + end end end @@ -944,6 +1011,13 @@ describe API::Users do expect(response).to have_http_status(404) end + + context "scopes" do + let(:path) { "/user/keys/#{key.id}" } + let(:api_call) { method(:api) } + + include_examples 'allows the "read_user" scope' + end end describe "POST /user/keys" do @@ -1033,6 +1107,13 @@ describe API::Users do expect(json_response).to be_an Array expect(json_response.first["email"]).to eq(email.email) end + + context "scopes" do + let(:path) { "/user/emails" } + let(:api_call) { method(:api) } + + include_examples 'allows the "read_user" scope' + end end end @@ -1065,6 +1146,13 @@ describe API::Users do expect(response).to have_http_status(404) end + + context "scopes" do + let(:path) { "/user/emails/#{email.id}" } + let(:api_call) { method(:api) } + + include_examples 'allows the "read_user" scope' + end end describe "POST /user/emails" do diff --git a/spec/requests/api/v3/users_spec.rb b/spec/requests/api/v3/users_spec.rb index 6d7401f9764..de7499a4e43 100644 --- a/spec/requests/api/v3/users_spec.rb +++ b/spec/requests/api/v3/users_spec.rb @@ -67,6 +67,19 @@ describe API::V3::Users do expect(json_response.first['title']).to eq(key.title) end end + + context "scopes" do + let(:user) { admin } + let(:path) { "/users/#{user.id}/keys" } + let(:api_call) { method(:v3_api) } + + before do + user.keys << key + user.save + end + + include_examples 'allows the "read_user" scope' + end end describe 'GET /user/:id/emails' do @@ -287,7 +300,7 @@ describe API::V3::Users do end it 'returns a 404 error if not found' do - get v3_api('/users/42/events', user) + get v3_api('/users/420/events', user) expect(response).to have_http_status(404) expect(json_response['message']).to eq('404 User Not Found') @@ -312,5 +325,13 @@ describe API::V3::Users do expect(json_response['is_admin']).to be_nil end + + context "scopes" do + let(:user) { admin } + let(:path) { '/users' } + let(:api_call) { method(:v3_api) } + + include_examples 'does not allow the "read_user" scope' + end end end diff --git a/spec/services/access_token_validation_service_spec.rb b/spec/services/access_token_validation_service_spec.rb index 87f093ee8ce..11225fad18a 100644 --- a/spec/services/access_token_validation_service_spec.rb +++ b/spec/services/access_token_validation_service_spec.rb @@ -2,40 +2,71 @@ require 'spec_helper' describe AccessTokenValidationService, services: true do describe ".include_any_scope?" do + let(:request) { double("request") } + it "returns true if the required scope is present in the token's scopes" do token = double("token", scopes: [:api, :read_user]) + scopes = [:api] - expect(described_class.new(token).include_any_scope?([:api])).to be(true) + expect(described_class.new(token, request: request).include_any_scope?(scopes)).to be(true) end it "returns true if more than one of the required scopes is present in the token's scopes" do token = double("token", scopes: [:api, :read_user, :other_scope]) + scopes = [:api, :other_scope] - expect(described_class.new(token).include_any_scope?([:api, :other_scope])).to be(true) + expect(described_class.new(token, request: request).include_any_scope?(scopes)).to be(true) end it "returns true if the list of required scopes is an exact match for the token's scopes" do token = double("token", scopes: [:api, :read_user, :other_scope]) + scopes = [:api, :read_user, :other_scope] - expect(described_class.new(token).include_any_scope?([:api, :read_user, :other_scope])).to be(true) + expect(described_class.new(token, request: request).include_any_scope?(scopes)).to be(true) end it "returns true if the list of required scopes contains all of the token's scopes, in addition to others" do token = double("token", scopes: [:api, :read_user]) + scopes = [:api, :read_user, :other_scope] - expect(described_class.new(token).include_any_scope?([:api, :read_user, :other_scope])).to be(true) + expect(described_class.new(token, request: request).include_any_scope?(scopes)).to be(true) end it 'returns true if the list of required scopes is blank' do token = double("token", scopes: []) + scopes = [] - expect(described_class.new(token).include_any_scope?([])).to be(true) + expect(described_class.new(token, request: request).include_any_scope?(scopes)).to be(true) end it "returns false if there are no scopes in common between the required scopes and the token scopes" do token = double("token", scopes: [:api, :read_user]) + scopes = [:other_scope] + + expect(described_class.new(token, request: request).include_any_scope?(scopes)).to be(false) + end + + context "conditions" do + it "ignores any scopes whose `if` condition returns false" do + token = double("token", scopes: [:api, :read_user]) + scopes = [API::Scope.new(:api, if: ->(_) { false })] + + expect(described_class.new(token, request: request).include_any_scope?(scopes)).to be(false) + end + + it "does not ignore scopes whose `if` condition is not set" do + token = double("token", scopes: [:api, :read_user]) + scopes = [API::Scope.new(:api, if: ->(_) { false }), :read_user] + + expect(described_class.new(token, request: request).include_any_scope?(scopes)).to be(true) + end + + it "does not ignore scopes whose `if` condition returns true" do + token = double("token", scopes: [:api, :read_user]) + scopes = [API::Scope.new(:api, if: ->(_) { true }), API::Scope.new(:read_user, if: ->(_) { false })] - expect(described_class.new(token).include_any_scope?([:other_scope])).to be(false) + expect(described_class.new(token, request: request).include_any_scope?(scopes)).to be(true) + end end end end diff --git a/spec/services/delete_merged_branches_service_spec.rb b/spec/services/delete_merged_branches_service_spec.rb index cae74df9c90..fe21ca0b3cb 100644 --- a/spec/services/delete_merged_branches_service_spec.rb +++ b/spec/services/delete_merged_branches_service_spec.rb @@ -24,6 +24,14 @@ describe DeleteMergedBranchesService, services: true do expect(project.repository.branch_names).to include('master') end + it 'keeps protected branches' do + create(:protected_branch, project: project, name: 'improve/awesome') + + service.execute + + expect(project.repository.branch_names).to include('improve/awesome') + end + context 'user without rights' do let(:user) { create(:user) } diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb index 711059208c1..19d9e4049fe 100644 --- a/spec/services/merge_requests/merge_service_spec.rb +++ b/spec/services/merge_requests/merge_service_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe MergeRequests::MergeService, services: true do let(:user) { create(:user) } let(:user2) { create(:user) } - let(:merge_request) { create(:merge_request, assignee: user2) } + let(:merge_request) { create(:merge_request, :simple, author: user2, assignee: user2) } let(:project) { merge_request.project } before do @@ -133,18 +133,65 @@ describe MergeRequests::MergeService, services: true do it { expect(todo).to be_done } end - context 'remove source branch by author' do - let(:service) do - merge_request.merge_params['force_remove_source_branch'] = '1' - merge_request.save! - MergeRequests::MergeService.new(project, user, commit_message: 'Awesome message') + context 'source branch removal' do + context 'when the source branch is protected' do + let(:service) do + MergeRequests::MergeService.new(project, user, should_remove_source_branch: '1') + end + + before do + create(:protected_branch, project: project, name: merge_request.source_branch) + end + + it 'does not delete the source branch' do + expect(DeleteBranchService).not_to receive(:new) + service.execute(merge_request) + end end - it 'removes the source branch' do - expect(DeleteBranchService).to receive(:new) - .with(merge_request.source_project, merge_request.author) - .and_call_original - service.execute(merge_request) + context 'when the source branch is the default branch' do + let(:service) do + MergeRequests::MergeService.new(project, user, should_remove_source_branch: '1') + end + + before do + allow(project).to receive(:root_ref?).with(merge_request.source_branch).and_return(true) + end + + it 'does not delete the source branch' do + expect(DeleteBranchService).not_to receive(:new) + service.execute(merge_request) + end + end + + context 'when the source branch can be removed' do + context 'when MR author set the source branch to be removed' do + let(:service) do + merge_request.merge_params['force_remove_source_branch'] = '1' + merge_request.save! + MergeRequests::MergeService.new(project, user, commit_message: 'Awesome message') + end + + it 'removes the source branch using the author user' do + expect(DeleteBranchService).to receive(:new) + .with(merge_request.source_project, merge_request.author) + .and_call_original + service.execute(merge_request) + end + end + + context 'when MR merger set the source branch to be removed' do + let(:service) do + MergeRequests::MergeService.new(project, user, commit_message: 'Awesome message', should_remove_source_branch: '1') + end + + it 'removes the source branch using the current user' do + expect(DeleteBranchService).to receive(:new) + .with(merge_request.source_project, user) + .and_call_original + service.execute(merge_request) + end + end end end diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb index 76c52d55ae5..441a5276c56 100644 --- a/spec/services/projects/transfer_service_spec.rb +++ b/spec/services/projects/transfer_service_spec.rb @@ -30,6 +30,12 @@ describe Projects::TransferService, services: true do transfer_project(project, user, group) end + it 'expires full_path cache' do + expect(project).to receive(:expires_full_path_cache) + + transfer_project(project, user, group) + end + it 'executes system hooks' do expect_any_instance_of(Projects::TransferService).to receive(:execute_system_hooks) diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb index c9e63efbc14..35373675894 100644 --- a/spec/services/quick_actions/interpret_service_spec.rb +++ b/spec/services/quick_actions/interpret_service_spec.rb @@ -359,18 +359,18 @@ describe QuickActions::InterpretService, services: true do let(:content) { "/assign @#{developer.username}" } context 'Issue' do - it 'fetches assignee and populates assignee_id if content contains /assign' do + it 'fetches assignee and populates assignee_ids if content contains /assign' do _, updates = service.execute(content, issue) - expect(updates).to eq(assignee_ids: [developer.id]) + expect(updates[:assignee_ids]).to match_array([developer.id]) end end context 'Merge Request' do - it 'fetches assignee and populates assignee_id if content contains /assign' do + it 'fetches assignee and populates assignee_ids if content contains /assign' do _, updates = service.execute(content, merge_request) - expect(updates).to eq(assignee_id: developer.id) + expect(updates).to eq(assignee_ids: [developer.id]) end end end @@ -383,7 +383,7 @@ describe QuickActions::InterpretService, services: true do end context 'Issue' do - it 'fetches assignee and populates assignee_id if content contains /assign' do + it 'fetches assignee and populates assignee_ids if content contains /assign' do _, updates = service.execute(content, issue) expect(updates[:assignee_ids]).to match_array([developer.id]) @@ -391,10 +391,10 @@ describe QuickActions::InterpretService, services: true do end context 'Merge Request' do - it 'fetches assignee and populates assignee_id if content contains /assign' do + it 'fetches assignee and populates assignee_ids if content contains /assign' do _, updates = service.execute(content, merge_request) - expect(updates).to eq(assignee_id: developer.id) + expect(updates).to eq(assignee_ids: [developer.id]) end end end @@ -422,11 +422,27 @@ describe QuickActions::InterpretService, services: true do end context 'Merge Request' do - it 'populates assignee_id: nil if content contains /unassign' do - merge_request.update(assignee_id: developer.id) + it 'populates assignee_ids: [] if content contains /unassign' do + merge_request.update(assignee_ids: [developer.id]) _, updates = service.execute(content, merge_request) - expect(updates).to eq(assignee_id: nil) + expect(updates).to eq(assignee_ids: []) + end + end + end + + context 'reassign command' do + let(:content) { '/reassign' } + + context 'Issue' do + it 'reassigns user if content contains /reassign @user' do + user = create(:user) + + issue.update(assignee_ids: [developer.id]) + + _, updates = service.execute("/reassign @#{user.username}", issue) + + expect(updates).to eq(assignee_ids: [user.id]) end end end diff --git a/spec/support/api/scopes/read_user_shared_examples.rb b/spec/support/api/scopes/read_user_shared_examples.rb new file mode 100644 index 00000000000..3bd589d64b9 --- /dev/null +++ b/spec/support/api/scopes/read_user_shared_examples.rb @@ -0,0 +1,79 @@ +shared_examples_for 'allows the "read_user" scope' do + context 'for personal access tokens' do + context 'when the requesting token has the "api" scope' do + let(:token) { create(:personal_access_token, scopes: ['api'], user: user) } + + it 'returns a "200" response' do + get api_call.call(path, user, personal_access_token: token) + + expect(response).to have_http_status(200) + end + end + + context 'when the requesting token has the "read_user" scope' do + let(:token) { create(:personal_access_token, scopes: ['read_user'], user: user) } + + it 'returns a "200" response' do + get api_call.call(path, user, personal_access_token: token) + + expect(response).to have_http_status(200) + end + end + + context 'when the requesting token does not have any required scope' do + let(:token) { create(:personal_access_token, scopes: ['read_registry'], user: user) } + + it 'returns a "401" response' do + get api_call.call(path, user, personal_access_token: token) + + expect(response).to have_http_status(401) + end + end + end + + context 'for doorkeeper (OAuth) tokens' do + let!(:application) { Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user) } + + context 'when the requesting token has the "api" scope' do + let!(:token) { Doorkeeper::AccessToken.create! application_id: application.id, resource_owner_id: user.id, scopes: "api" } + + it 'returns a "200" response' do + get api_call.call(path, user, oauth_access_token: token) + + expect(response).to have_http_status(200) + end + end + + context 'when the requesting token has the "read_user" scope' do + let!(:token) { Doorkeeper::AccessToken.create! application_id: application.id, resource_owner_id: user.id, scopes: "read_user" } + + it 'returns a "200" response' do + get api_call.call(path, user, oauth_access_token: token) + + expect(response).to have_http_status(200) + end + end + + context 'when the requesting token does not have any required scope' do + let!(:token) { Doorkeeper::AccessToken.create! application_id: application.id, resource_owner_id: user.id, scopes: "invalid" } + + it 'returns a "403" response' do + get api_call.call(path, user, oauth_access_token: token) + + expect(response).to have_http_status(403) + end + end + end +end + +shared_examples_for 'does not allow the "read_user" scope' do + context 'when the requesting token has the "read_user" scope' do + let(:token) { create(:personal_access_token, scopes: ['read_user'], user: user) } + + it 'returns a "401" response' do + post api_call.call(path, user, personal_access_token: token), attributes_for(:user, projects_limit: 3) + + expect(response).to have_http_status(401) + end + end +end diff --git a/spec/support/api_helpers.rb b/spec/support/api_helpers.rb index 35d1e1cfc7d..ac0aaa524b7 100644 --- a/spec/support/api_helpers.rb +++ b/spec/support/api_helpers.rb @@ -17,14 +17,18 @@ module ApiHelpers # => "/api/v2/issues?foo=bar&private_token=..." # # Returns the relative path to the requested API resource - def api(path, user = nil, version: API::API.version) + def api(path, user = nil, version: API::API.version, personal_access_token: nil, oauth_access_token: nil) "/api/#{version}#{path}" + # Normalize query string (path.index('?') ? '' : '?') + + if personal_access_token.present? + "&private_token=#{personal_access_token.token}" + elsif oauth_access_token.present? + "&access_token=#{oauth_access_token.token}" # Append private_token if given a User object - if user.respond_to?(:private_token) + elsif user.respond_to?(:private_token) "&private_token=#{user.private_token}" else '' @@ -32,8 +36,14 @@ module ApiHelpers end # Temporary helper method for simplifying V3 exclusive API specs - def v3_api(path, user = nil) - api(path, user, version: 'v3') + def v3_api(path, user = nil, personal_access_token: nil, oauth_access_token: nil) + api( + path, + user, + version: 'v3', + personal_access_token: personal_access_token, + oauth_access_token: oauth_access_token + ) end def ci_api(path, user = nil) diff --git a/spec/support/capybara_helpers.rb b/spec/support/capybara_helpers.rb index b57a3493aff..3eb7bea3227 100644 --- a/spec/support/capybara_helpers.rb +++ b/spec/support/capybara_helpers.rb @@ -35,6 +35,11 @@ module CapybaraHelpers visit 'about:blank' visit url end + + # Simulate a browser restart by clearing the session cookie. + def clear_browser_session + page.driver.remove_cookie('_gitlab_session') + end end RSpec.configure do |config| diff --git a/spec/support/login_helpers.rb b/spec/support/login_helpers.rb index 4c88958264b..99e7806353d 100644 --- a/spec/support/login_helpers.rb +++ b/spec/support/login_helpers.rb @@ -62,6 +62,16 @@ module LoginHelpers Thread.current[:current_user] = user end + def login_via(provider, user, uid, remember_me: false) + mock_auth_hash(provider, uid, user.email) + visit new_user_session_path + expect(page).to have_content('Sign in with') + + check 'Remember Me' if remember_me + + click_link "oauth-login-#{provider}" + end + def mock_auth_hash(provider, uid, email) # The mock_auth configuration allows you to set per-provider (or default) # authentication hashes to return during integration testing. @@ -108,6 +118,7 @@ module LoginHelpers end allow(Gitlab::OAuth::Provider).to receive_messages(providers: [:saml], config_for: mock_saml_config) stub_omniauth_setting(messages) - expect_any_instance_of(Object).to receive(:omniauth_authorize_path).with(:user, "saml").and_return('/users/auth/saml') + allow_any_instance_of(Object).to receive(:user_saml_omniauth_authorize_path).and_return('/users/auth/saml') + allow_any_instance_of(Object).to receive(:omniauth_authorize_path).with(:user, "saml").and_return('/users/auth/saml') end end diff --git a/spec/support/matchers/be_utf8.rb b/spec/support/matchers/be_utf8.rb new file mode 100644 index 00000000000..ea806352422 --- /dev/null +++ b/spec/support/matchers/be_utf8.rb @@ -0,0 +1,9 @@ +RSpec::Matchers.define :be_utf8 do |_| + match do |actual| + actual.is_a?(String) && actual.encoding == Encoding.find('UTF-8') + end + + description do + "be a String with encoding UTF-8" + end +end |