diff options
author | Achilleas Pipinellis <axil@gitlab.com> | 2017-09-18 05:38:54 +0000 |
---|---|---|
committer | Achilleas Pipinellis <axil@gitlab.com> | 2017-09-18 05:38:54 +0000 |
commit | 99b95ea97cd016e07e40cc361292be6a155325f8 (patch) | |
tree | cf5316258cab80df747d119c8b397dc3a4ad8cd9 | |
parent | 8211ce1906217b4562d52c6d4c2192bba1f9764d (diff) | |
parent | 2defc7b931ac46603780944907d7e19233ad1e97 (diff) | |
download | gitlab-ce-99b95ea97cd016e07e40cc361292be6a155325f8.tar.gz |
Merge branch 'master' into 'docs-replace-pipelines-cicd'
# Conflicts:
# doc/ci/variables/README.md
384 files changed, 7147 insertions, 4667 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f27d809af3c..0520f3b1c37 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -191,6 +191,9 @@ review-docs-deploy: stage: build environment: name: review-docs/$CI_COMMIT_REF_NAME + # DOCS_REVIEW_APPS_DOMAIN and DOCS_GITLAB_REPO_SUFFIX are secret variables + # Discussion: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14236/diffs#note_40140693 + url: http://$CI_COMMIT_REF_SLUG-built-from-ce-ee.$DOCS_REVIEW_APPS_DOMAIN/$DOCS_GITLAB_REPO_SUFFIX on_stop: review-docs-cleanup script: - gem install gitlab --no-doc diff --git a/.rubocop.yml b/.rubocop.yml index 4640681379a..dbeb1880d39 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -643,7 +643,7 @@ Metrics/ClassLength: # of test cases needed to validate a method. Metrics/CyclomaticComplexity: Enabled: true - Max: 14 + Max: 13 # Limit lines to 80 characters. Metrics/LineLength: @@ -665,7 +665,7 @@ Metrics/ParameterLists: # A complexity metric geared towards measuring complexity for a human reader. Metrics/PerceivedComplexity: Enabled: true - Max: 17 + Max: 15 # Lint ######################################################################## diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1f25171e8a6..dfb2ce0099a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -286,7 +286,10 @@ might be edited to make them small and simple. Please submit Feature Proposals using the ['Feature Proposal' issue template](.gitlab/issue_templates/Feature Proposal.md) provided on the issue tracker. -For changes in the interface, it can be helpful to create a mockup first. +For changes in the interface, it is helpful to include a mockup. Issues that add to, or change, the interface should +be given the ~"UX" label. This will allow the UX team to provide input and guidance. You may +need to ask one of the [core team] members to add the label, if you do not have permissions to do it by yourself. + If you want to create something yourself, consider opening an issue first to discuss whether it is interesting to include this in GitLab. @@ -407,4 +407,4 @@ gem 'flipper-active_record', '~> 0.10.2' # Structured logging gem 'lograge', '~> 0.5' -gem 'grape_logging', '~> 1.6' +gem 'grape_logging', '~> 1.7' diff --git a/Gemfile.lock b/Gemfile.lock index d7e1c7581d5..e10db81d0c9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -355,9 +355,9 @@ GEM activesupport grape (>= 0.16.0) rake - grape_logging (1.6.0) + grape_logging (1.7.0) grape - grpc (1.4.5) + grpc (1.6.0) google-protobuf (~> 3.1) googleauth (~> 0.5.1) haml (4.0.7) @@ -1037,7 +1037,7 @@ DEPENDENCIES grape (~> 1.0) grape-entity (~> 0.6.0) grape-route-helpers (~> 2.1.0) - grape_logging (~> 1.6) + grape_logging (~> 1.7) haml_lint (~> 0.26.0) hamlit (~> 2.6.1) hashie-forbidden_attributes diff --git a/app/assets/javascripts/branches/branches_delete_modal.js b/app/assets/javascripts/branches/branches_delete_modal.js index af8bcdc1794..cbc28374b80 100644 --- a/app/assets/javascripts/branches/branches_delete_modal.js +++ b/app/assets/javascripts/branches/branches_delete_modal.js @@ -7,6 +7,7 @@ class DeleteModal { this.$branchName = $('.js-branch-name', this.$modal); this.$confirmInput = $('.js-delete-branch-input', this.$modal); this.$deleteBtn = $('.js-delete-branch', this.$modal); + this.$notMerged = $('.js-not-merged', this.$modal); this.bindEvents(); } @@ -16,8 +17,10 @@ class DeleteModal { } setModalData(e) { - this.branchName = e.currentTarget.dataset.branchName || ''; - this.deletePath = e.currentTarget.dataset.deletePath || ''; + const branchData = e.currentTarget.dataset; + this.branchName = branchData.branchName || ''; + this.deletePath = branchData.deletePath || ''; + this.isMerged = !!branchData.isMerged; this.updateModal(); } @@ -30,6 +33,7 @@ class DeleteModal { this.$confirmInput.val(''); this.$deleteBtn.attr('href', this.deletePath); this.$deleteBtn.attr('disabled', true); + this.$notMerged.toggleClass('hidden', this.isMerged); } } diff --git a/app/assets/javascripts/commons/polyfills.js b/app/assets/javascripts/commons/polyfills.js index b78089525cc..cb5a9a9f6b5 100644 --- a/app/assets/javascripts/commons/polyfills.js +++ b/app/assets/javascripts/commons/polyfills.js @@ -12,4 +12,5 @@ import 'core-js/fn/symbol'; // Browser polyfills import './polyfills/custom_event'; import './polyfills/element'; +import './polyfills/event'; import './polyfills/nodelist'; diff --git a/app/assets/javascripts/commons/polyfills/custom_event.js b/app/assets/javascripts/commons/polyfills/custom_event.js index aea61b82d03..db51ade61ae 100644 --- a/app/assets/javascripts/commons/polyfills/custom_event.js +++ b/app/assets/javascripts/commons/polyfills/custom_event.js @@ -1,7 +1,12 @@ if (typeof window.CustomEvent !== 'function') { window.CustomEvent = function CustomEvent(event, params) { const evt = document.createEvent('CustomEvent'); - const evtParams = params || { bubbles: false, cancelable: false, detail: undefined }; + const evtParams = { + bubbles: false, + cancelable: false, + detail: undefined, + ...params, + }; evt.initCustomEvent(event, evtParams.bubbles, evtParams.cancelable, evtParams.detail); return evt; }; diff --git a/app/assets/javascripts/commons/polyfills/event.js b/app/assets/javascripts/commons/polyfills/event.js new file mode 100644 index 00000000000..ff5b9a1982f --- /dev/null +++ b/app/assets/javascripts/commons/polyfills/event.js @@ -0,0 +1,18 @@ +/** + * Polyfill for IE11 support. + * new Event() is not supported by IE11. + * Although `initEvent` is deprecated for modern browsers it is the one supported by IE + */ +if (typeof window.Event !== 'function') { + window.Event = function Event(event, params) { + const evt = document.createEvent('Event'); + const evtParams = { + bubbles: false, + cancelable: false, + ...params, + }; + evt.initEvent(event, evtParams.bubbles, evtParams.cancelable); + return evt; + }; + window.Event.prototype = Event; +} diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js b/app/assets/javascripts/filtered_search/dropdown_user.js index 7246ccbb281..720fbc87ea0 100644 --- a/app/assets/javascripts/filtered_search/dropdown_user.js +++ b/app/assets/javascripts/filtered_search/dropdown_user.js @@ -15,6 +15,7 @@ class DropdownUser extends gl.FilteredSearchDropdown { params: { per_page: 20, active: true, + group_id: this.getGroupId(), project_id: this.getProjectId(), current_user: true, }, @@ -47,6 +48,10 @@ class DropdownUser extends gl.FilteredSearchDropdown { super.renderContent(forceShowList); } + getGroupId() { + return this.input.getAttribute('data-group-id'); + } + getProjectId() { return this.input.getAttribute('data-project-id'); } diff --git a/app/assets/javascripts/fly_out_nav.js b/app/assets/javascripts/fly_out_nav.js index 4b19f7b4188..157280d66e3 100644 --- a/app/assets/javascripts/fly_out_nav.js +++ b/app/assets/javascripts/fly_out_nav.js @@ -77,10 +77,11 @@ export const hideMenu = (el) => { export const moveSubItemsToPosition = (el, subItems) => { const boundingRect = el.getBoundingClientRect(); const top = calculateTop(boundingRect, subItems.offsetHeight); + const left = sidebar ? sidebar.offsetWidth : 50; const isAbove = top < boundingRect.top; subItems.classList.add('fly-out-list'); - subItems.style.transform = `translate3d(0, ${Math.floor(top) - headerHeight}px, 0)`; // eslint-disable-line no-param-reassign + subItems.style.transform = `translate3d(${left}px, ${Math.floor(top) - headerHeight}px, 0)`; // eslint-disable-line no-param-reassign const subItemsRect = subItems.getBoundingClientRect(); @@ -148,7 +149,7 @@ export const documentMouseMove = (e) => { export const subItemsMouseLeave = (relatedTarget) => { clearTimeout(timeoutId); - if (!relatedTarget.closest(`.${IS_OVER_CLASS}`)) { + if (relatedTarget && !relatedTarget.closest(`.${IS_OVER_CLASS}`)) { hideMenu(currentOpenMenu); } }; diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue index e115ee40219..06f6ec241f4 100644 --- a/app/assets/javascripts/issue_show/components/app.vue +++ b/app/assets/javascripts/issue_show/components/app.vue @@ -72,10 +72,6 @@ export default { required: false, default: () => [], }, - isConfidential: { - type: Boolean, - required: true, - }, markdownPreviewPath: { type: String, required: true, @@ -131,7 +127,6 @@ export default { this.showForm = true; this.store.setFormState({ title: this.state.titleText, - confidential: this.isConfidential, description: this.state.descriptionText, lockedWarningVisible: false, updateLoading: false, @@ -147,8 +142,6 @@ export default { .then((data) => { if (location.pathname !== data.web_url) { gl.utils.visitUrl(data.web_url); - } else if (data.confidential !== this.isConfidential) { - gl.utils.visitUrl(location.pathname); } return this.service.getData(); diff --git a/app/assets/javascripts/issue_show/components/fields/confidential_checkbox.vue b/app/assets/javascripts/issue_show/components/fields/confidential_checkbox.vue deleted file mode 100644 index a0ff08e9111..00000000000 --- a/app/assets/javascripts/issue_show/components/fields/confidential_checkbox.vue +++ /dev/null @@ -1,23 +0,0 @@ -<script> - export default { - props: { - formState: { - type: Object, - required: true, - }, - }, - }; -</script> - -<template> - <fieldset class="checkbox"> - <label for="issue-confidential"> - <input - type="checkbox" - value="1" - id="issue-confidential" - v-model="formState.confidential" /> - This issue is confidential and should only be visible to team members with at least Reporter access. - </label> - </fieldset> -</template> diff --git a/app/assets/javascripts/issue_show/components/form.vue b/app/assets/javascripts/issue_show/components/form.vue index 6a2dd502fe2..28bf6c67ea5 100644 --- a/app/assets/javascripts/issue_show/components/form.vue +++ b/app/assets/javascripts/issue_show/components/form.vue @@ -4,7 +4,6 @@ import descriptionField from './fields/description.vue'; import editActions from './edit_actions.vue'; import descriptionTemplate from './fields/description_template.vue'; - import confidentialCheckbox from './fields/confidential_checkbox.vue'; export default { props: { @@ -44,7 +43,6 @@ descriptionField, descriptionTemplate, editActions, - confidentialCheckbox, }, computed: { hasIssuableTemplates() { @@ -81,8 +79,6 @@ :form-state="formState" :markdown-preview-path="markdownPreviewPath" :markdown-docs-path="markdownDocsPath" /> - <confidential-checkbox - :form-state="formState" /> <edit-actions :form-state="formState" :can-destroy="canDestroy" /> diff --git a/app/assets/javascripts/issue_show/index.js b/app/assets/javascripts/issue_show/index.js index 8053ef57e6c..aca9dec2a96 100644 --- a/app/assets/javascripts/issue_show/index.js +++ b/app/assets/javascripts/issue_show/index.js @@ -35,7 +35,6 @@ document.addEventListener('DOMContentLoaded', () => { initialDescriptionHtml: this.initialDescriptionHtml, initialDescriptionText: this.initialDescriptionText, issuableTemplates: this.issuableTemplates, - isConfidential: this.isConfidential, markdownPreviewPath: this.markdownPreviewPath, markdownDocsPath: this.markdownDocsPath, projectPath: this.projectPath, diff --git a/app/assets/javascripts/issue_show/stores/index.js b/app/assets/javascripts/issue_show/stores/index.js index f4639e9ed2a..af8b0414266 100644 --- a/app/assets/javascripts/issue_show/stores/index.js +++ b/app/assets/javascripts/issue_show/stores/index.js @@ -3,7 +3,6 @@ export default class Store { this.state = initialState; this.formState = { title: '', - confidential: false, description: '', lockedWarningVisible: false, updateLoading: false, diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index 7d7f91227f9..2538d9c2093 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -127,13 +127,6 @@ import DropdownUtils from './filtered_search/dropdown_utils'; $('.has-tooltip', $value).tooltip({ container: 'body' }); - return $value.find('a').each(function(i) { - return setTimeout((function(_this) { - return function() { - return gl.animate.animate($(_this), 'pulse'); - }; - })(this), 200 * i); - }); }); }; $dropdown.glDropdown({ diff --git a/app/assets/javascripts/lib/utils/animate.js b/app/assets/javascripts/lib/utils/animate.js deleted file mode 100644 index d93c1d0da59..00000000000 --- a/app/assets/javascripts/lib/utils/animate.js +++ /dev/null @@ -1,49 +0,0 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-param-reassign, no-void, prefer-template, no-var, new-cap, prefer-arrow-callback, consistent-return, max-len */ -(function() { - (function(w) { - if (w.gl == null) { - w.gl = {}; - } - if (gl.animate == null) { - gl.animate = {}; - } - gl.animate.animate = function($el, animation, options, done) { - if ((options != null ? options.cssStart : void 0) != null) { - $el.css(options.cssStart); - } - $el.removeClass(animation + ' animated').addClass(animation + ' animated').one('webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend', function() { - $(this).removeClass(animation + ' animated'); - if (done != null) { - done(); - } - if ((options != null ? options.cssEnd : void 0) != null) { - $el.css(options.cssEnd); - } - }); - }; - gl.animate.animateEach = function($els, animation, time, options, done) { - var dfd; - dfd = $.Deferred(); - if (!$els.length) { - dfd.resolve(); - } - $els.each(function(i) { - setTimeout((function(_this) { - return function() { - var $this; - $this = $(_this); - return gl.animate.animate($this, animation, options, function() { - if (i === $els.length - 1) { - dfd.resolve(); - if (done != null) { - return done(); - } - } - }); - }; - })(this), time * i); - }); - return dfd.promise(); - }; - })(window); -}).call(window); diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 0bc31a56684..0f84470828a 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -39,7 +39,6 @@ import './commit/file'; import './commit/image_file'; // lib/utils -import './lib/utils/animate'; import './lib/utils/bootstrap_linked_tabs'; import './lib/utils/common_utils'; import './lib/utils/datetime_utility'; diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js index 04579058688..4675b1fcb8f 100644 --- a/app/assets/javascripts/milestone_select.js +++ b/app/assets/javascripts/milestone_select.js @@ -45,7 +45,7 @@ import _ from 'underscore'; if (issueUpdateURL) { milestoneLinkTemplate = _.template('<a href="/<%- full_path %>/milestones/<%- iid %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>'); milestoneLinkNoneTemplate = '<span class="no-value">None</span>'; - collapsedSidebarLabelTemplate = _.template('<span class="has-tooltip" data-container="body" title="<%- remaining %>" data-placement="left"> <%- title %> </span>'); + collapsedSidebarLabelTemplate = _.template('<span class="has-tooltip" data-container="body" title="<%- name %><br /><%- remaining %>" data-placement="left" data-html="true"> <%- title %> </span>'); } return $dropdown.glDropdown({ showMenuAbove: showMenuAbove, @@ -208,6 +208,7 @@ import _ from 'underscore'; if (data.milestone != null) { data.milestone.full_path = _this.currentProject.full_path; data.milestone.remaining = gl.utils.timeFor(data.milestone.due_date); + data.milestone.name = data.milestone.title; $value.html(milestoneLinkTemplate(data.milestone)); return $sidebarCollapsedValue.find('span').html(collapsedSidebarLabelTemplate(data.milestone)); } else { diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index b596c4f383f..5d96b193fce 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -1,7 +1,6 @@ <script> /* global Flash */ import _ from 'underscore'; - import statusCodes from '../../lib/utils/http_status'; import MonitoringService from '../services/monitoring_service'; import GraphGroup from './graph_group.vue'; import Graph from './graph.vue'; @@ -21,10 +20,9 @@ hasMetrics: gl.utils.convertPermissionToBoolean(metricsData.hasMetrics), documentationPath: metricsData.documentationPath, settingsPath: metricsData.settingsPath, - endpoint: metricsData.additionalMetrics, + metricsEndpoint: metricsData.additionalMetrics, deploymentEndpoint: metricsData.deploymentEndpoint, showEmptyState: true, - backOffRequestCounter: 0, updateAspectRatio: false, updatedAspectRatios: 0, resizeThrottled: {}, @@ -39,50 +37,16 @@ methods: { getGraphsData() { - const maxNumberOfRequests = 3; this.state = 'loading'; - gl.utils.backOff((next, stop) => { - this.service.get().then((resp) => { - if (resp.status === statusCodes.NO_CONTENT) { - this.backOffRequestCounter = this.backOffRequestCounter += 1; - if (this.backOffRequestCounter < maxNumberOfRequests) { - next(); - } else { - stop(new Error('Failed to connect to the prometheus server')); - } - } else { - stop(resp); - } - }).catch(stop); - }) - .then((resp) => { - if (resp.status === statusCodes.NO_CONTENT) { - this.state = 'unableToConnect'; - return false; - } - return resp.json(); - }) - .then((metricGroupsData) => { - if (!metricGroupsData) return false; - this.store.storeMetrics(metricGroupsData.data); - return this.getDeploymentData(); - }) - .then((deploymentData) => { - if (deploymentData !== false) { - this.store.storeDeploymentData(deploymentData.deployments); - this.showEmptyState = false; - } - return {}; - }) - .catch(() => { - this.state = 'unableToConnect'; - }); - }, - - getDeploymentData() { - return this.service.getDeploymentData(this.deploymentEndpoint) - .then(resp => resp.json()) - .catch(() => new Flash('Error getting deployment information.')); + Promise.all([ + this.service.getGraphsData() + .then(data => this.store.storeMetrics(data)), + this.service.getDeploymentData() + .then(data => this.store.storeDeploymentData(data)) + .catch(() => new Flash('Error getting deployment information.')), + ]) + .then(() => { this.showEmptyState = false; }) + .catch(() => { this.state = 'unableToConnect'; }); }, resize() { @@ -99,7 +63,10 @@ }, created() { - this.service = new MonitoringService(this.endpoint); + this.service = new MonitoringService({ + metricsEndpoint: this.metricsEndpoint, + deploymentEndpoint: this.deploymentEndpoint, + }); eventHub.$on('toggleAspectRatio', this.toggleAspectRatio); }, diff --git a/app/assets/javascripts/monitoring/services/monitoring_service.js b/app/assets/javascripts/monitoring/services/monitoring_service.js index 1e9ae934853..4ed651d5740 100644 --- a/app/assets/javascripts/monitoring/services/monitoring_service.js +++ b/app/assets/javascripts/monitoring/services/monitoring_service.js @@ -1,19 +1,54 @@ import Vue from 'vue'; import VueResource from 'vue-resource'; +import statusCodes from '../../lib/utils/http_status'; Vue.use(VueResource); +const MAX_REQUESTS = 3; + +function backOffRequest(makeRequestCallback) { + let requestCounter = 0; + return gl.utils.backOff((next, stop) => { + makeRequestCallback().then((resp) => { + if (resp.status === statusCodes.NO_CONTENT) { + requestCounter += 1; + if (requestCounter < MAX_REQUESTS) { + next(); + } else { + stop(new Error('Failed to connect to the prometheus server')); + } + } else { + stop(resp); + } + }).catch(stop); + }); +} + export default class MonitoringService { - constructor(endpoint) { - this.graphs = Vue.resource(endpoint); + constructor({ metricsEndpoint, deploymentEndpoint }) { + this.metricsEndpoint = metricsEndpoint; + this.deploymentEndpoint = deploymentEndpoint; } - get() { - return this.graphs.get(); + getGraphsData() { + return backOffRequest(() => Vue.http.get(this.metricsEndpoint)) + .then(resp => resp.json()) + .then((response) => { + if (!response || !response.data) { + throw new Error('Unexpected metrics data response from prometheus endpoint'); + } + return response.data; + }); } - // eslint-disable-next-line class-methods-use-this - getDeploymentData(endpoint) { - return Vue.http.get(endpoint); + getDeploymentData() { + return backOffRequest(() => Vue.http.get(this.deploymentEndpoint)) + .then(resp => resp.json()) + .then((response) => { + if (!response || !response.deployments) { + throw new Error('Unexpected deployment data response from prometheus endpoint'); + } + return response.deployments; + }); } } diff --git a/app/assets/javascripts/new_sidebar.js b/app/assets/javascripts/new_sidebar.js index fbe474f2f61..f2eb2338a1e 100644 --- a/app/assets/javascripts/new_sidebar.js +++ b/app/assets/javascripts/new_sidebar.js @@ -15,7 +15,6 @@ export default class NewNavSidebar { this.$openSidebar = $('.toggle-mobile-nav'); this.$closeSidebar = $('.close-nav-button'); this.$sidebarToggle = $('.js-toggle-sidebar'); - this.$topLevelLinks = $('.sidebar-top-level-items > li > a'); } bindEvents() { @@ -56,10 +55,6 @@ export default class NewNavSidebar { this.$page.toggleClass('page-with-icon-sidebar', breakpoint === 'sm' ? true : collapsed); } NewNavSidebar.setCollapsedCookie(collapsed); - - this.$topLevelLinks.attr('title', function updateTopLevelTitle() { - return collapsed ? this.getAttribute('aria-label') : ''; - }); } render() { @@ -68,7 +63,7 @@ export default class NewNavSidebar { if (breakpoint === 'sm' || breakpoint === 'md') { this.toggleCollapsedSidebar(true); } else if (breakpoint === 'lg') { - const collapse = this.$sidebar.hasClass('sidebar-icons-only'); + const collapse = Cookies.get('sidebar_collapsed') === 'true'; this.toggleCollapsedSidebar(collapse); } } diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index c0524bf6aa3..35e7a10379f 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -19,6 +19,7 @@ @import "framework/flash"; @import "framework/forms"; @import "framework/gfm"; +@import "framework/gitlab-theme"; @import "framework/header"; @import "framework/highlight"; @import "framework/issue_box"; diff --git a/app/assets/stylesheets/framework/emojis.scss b/app/assets/stylesheets/framework/emojis.scss index 2d6bc17d4ff..527e7d57c5c 100644 --- a/app/assets/stylesheets/framework/emojis.scss +++ b/app/assets/stylesheets/framework/emojis.scss @@ -1,4 +1,5 @@ gl-emoji { + font-style: normal; display: inline-flex; vertical-align: middle; font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index 8ad082f7a65..588ec1ff3bc 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -17,8 +17,11 @@ max-width: $limited-layout-width-sm; margin-left: auto; margin-right: auto; - padding-top: 64px; - padding-bottom: 64px; + + @media (min-width: $screen-md-min) { + padding-top: 64px; + padding-bottom: 64px; + } } } diff --git a/app/assets/stylesheets/framework/gitlab-theme.scss b/app/assets/stylesheets/framework/gitlab-theme.scss new file mode 100644 index 00000000000..f844d6f1d5a --- /dev/null +++ b/app/assets/stylesheets/framework/gitlab-theme.scss @@ -0,0 +1,281 @@ +/** + * Styles the GitLab application with a specific color theme + */ + +@mixin gitlab-theme($color-100, $color-200, $color-500, $color-700, $color-800, $color-900, $color-alternate) { + // Header + + header.navbar-gitlab-new { + background: linear-gradient(to right, $color-900, $color-800); + + .navbar-collapse { + color: $color-200; + } + + .container-fluid { + .navbar-toggle { + border-left: 1px solid lighten($color-700, 10%); + } + } + + .navbar-sub-nav, + .navbar-nav { + > li { + > a:hover, + > a:focus { + background-color: rgba($color-200, .2); + } + + &.active > a, + &.dropdown.open > a { + color: $color-900; + background-color: $color-alternate; + + svg { + fill: currentColor; + } + } + + &.line-separator { + border-left: 1px solid rgba($color-200, .2); + } + } + } + + .navbar-sub-nav { + color: $color-200; + } + + .nav { + > li { + color: $color-200; + + > a { + svg { + fill: $color-200; + } + + &.header-user-dropdown-toggle { + .header-user-avatar { + border-color: $color-200; + } + } + + &:hover, + &:focus { + @media (min-width: $screen-sm-min) { + background-color: rgba($color-200, .2); + } + + svg { + fill: currentColor; + } + } + } + + &.active > a, + &.dropdown.open > a { + color: $color-900; + background-color: $color-alternate; + + &:hover { + svg { + fill: $color-900; + } + } + } + + .impersonated-user, + .impersonated-user:hover { + svg { + fill: $color-900; + } + } + } + } + } + + .title { + > a { + &:hover, + &:focus { + background-color: rgba($color-200, .2); + } + } + } + + .search { + form { + background-color: rgba($color-200, .2); + + &:hover { + background-color: rgba($color-200, .3); + } + } + + .location-badge { + color: $color-100; + background-color: rgba($color-200, .1); + border-right: 1px solid $color-800; + } + + .search-input::placeholder { + color: rgba($color-200, .8); + } + + .search-input-wrap { + .search-icon, + .clear-icon { + color: rgba($color-200, .8); + } + } + + &.search-active { + form { + background-color: $white-light; + } + + .location-badge { + color: $gl-text-color; + } + + .search-input-wrap { + .search-icon { + color: rgba($color-200, .8); + } + } + } + } + + .btn-sign-in { + background-color: $color-100; + color: $color-900; + } + + + // Sidebar + .nav-sidebar li.active { + box-shadow: inset 4px 0 0 $color-700; + + > a { + color: $color-800; + } + + svg { + fill: $color-800; + } + } + + .sidebar-top-level-items > li.active .badge { + color: $color-800; + } + + .nav-links li.active a { + border-bottom-color: $color-500; + + .badge { + font-weight: $gl-font-weight-bold; + } + } +} + + +body { + &.ui_indigo { + @include gitlab-theme($indigo-100, $indigo-200, $indigo-500, $indigo-700, $indigo-800, $indigo-900, $white-light); + } + + &.ui_dark { + @include gitlab-theme($theme-gray-100, $theme-gray-200, $theme-gray-500, $theme-gray-700, $theme-gray-800, $theme-gray-900, $white-light); + } + + &.ui_blue { + @include gitlab-theme($theme-blue-100, $theme-blue-200, $theme-blue-500, $theme-blue-700, $theme-blue-800, $theme-blue-900, $white-light); + } + + &.ui_green { + @include gitlab-theme($theme-green-100, $theme-green-200, $theme-green-500, $theme-green-700, $theme-green-800, $theme-green-900, $white-light); + } + + &.ui_light { + @include gitlab-theme($theme-gray-900, $theme-gray-700, $theme-gray-800, $theme-gray-700, $theme-gray-700, $theme-gray-100, $theme-gray-700); + + header.navbar-gitlab-new { + background: $theme-gray-100; + box-shadow: 0 2px 0 0 $border-color; + + .logo-text svg { + fill: $theme-gray-900; + } + + .navbar-sub-nav, + .navbar-nav { + > li { + > a:hover, + > a:focus { + color: $theme-gray-900; + } + + &.active > a { + color: $white-light; + + &:hover { + color: $white-light; + } + } + } + } + + .container-fluid { + .navbar-toggle, + .navbar-toggle:hover { + color: $theme-gray-700; + border-left: 1px solid $theme-gray-200; + } + } + } + + .search { + form { + background-color: $white-light; + box-shadow: inset 0 0 0 1px $border-color; + + &:hover { + background-color: $white-light; + box-shadow: inset 0 0 0 1px $blue-100; + + .location-badge { + box-shadow: inset 0 0 0 1px $blue-100; + } + } + } + + .search-input-wrap { + .search-icon { + color: $theme-gray-200; + } + } + + .location-badge { + color: $theme-gray-700; + box-shadow: inset 0 0 0 1px $border-color; + background-color: $nav-badge-bg; + border-right: 0; + } + } + + .nav-sidebar li.active { + > a { + color: $theme-gray-900; + } + + svg { + fill: $theme-gray-900; + } + } + + .sidebar-top-level-items > li.active .badge { + color: $theme-gray-900; + } + } +} diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index b00a2d053e2..ab3c34df1fb 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -111,7 +111,6 @@ header { svg { height: 16px; width: 23px; - fill: currentColor; } } diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index 5ffa67a1220..2f7717760ec 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -328,7 +328,7 @@ border-bottom: 1px solid $border-color; transition: padding $sidebar-transition-duration; text-align: center; - margin-top: $header-height; + margin-top: $new-navbar-height; .container-fluid { position: relative; diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index e300b006026..a3da9fd44e8 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -13,6 +13,7 @@ $sidebar-breakpoint: 1024px; $darken-normal-factor: 7%; $darken-dark-factor: 10%; $darken-border-factor: 5%; +$darken-border-dashed-factor: 25%; $white-light: #fff; $white-normal: #f0f0f0; @@ -74,6 +75,8 @@ $red-700: #a62d19; $red-800: #8b2615; $red-900: #711e11; +// GitLab themes + $indigo-50: #f7f7ff; $indigo-100: #ebebfa; $indigo-200: #d1d1f0; @@ -86,6 +89,43 @@ $indigo-800: #393982; $indigo-900: #292961; $indigo-950: #1a1a40; +$theme-gray-50: #fafafa; +$theme-gray-100: #f2f2f2; +$theme-gray-200: #dfdfdf; +$theme-gray-300: #cccccc; +$theme-gray-400: #bababa; +$theme-gray-500: #a7a7a7; +$theme-gray-600: #949494; +$theme-gray-700: #707070; +$theme-gray-800: #4f4f4f; +$theme-gray-900: #2e2e2e; +$theme-gray-950: #1f1f1f; + +$theme-blue-50: #f4f8fc; +$theme-blue-100: #e6edf5; +$theme-blue-200: #c8d7e6; +$theme-blue-300: #97b3cf; +$theme-blue-400: #648cb4; +$theme-blue-500: #4a79a8; +$theme-blue-600: #3e6fa0; +$theme-blue-700: #305c88; +$theme-blue-800: #25496e; +$theme-blue-900: #1a3652; +$theme-blue-950: #0f2235; + +$theme-green-50: #f2faf6; +$theme-green-100: #e4f3ea; +$theme-green-200: #c0dfcd; +$theme-green-300: #8ac2a1; +$theme-green-400: #52a274; +$theme-green-500: #35935c; +$theme-green-600: #288a50; +$theme-green-700: #1c7441; +$theme-green-800: #145d33; +$theme-green-900: #0d4524; +$theme-green-950: #072d16; + + $black: #000; $black-transparent: rgba(0, 0, 0, 0.3); $almost-black: #242424; @@ -95,6 +135,7 @@ $border-white-normal: darken($white-normal, $darken-border-factor); $border-gray-light: darken($gray-light, $darken-border-factor); $border-gray-normal: darken($gray-normal, $darken-border-factor); +$border-gray-normal-dashed: darken($gray-normal, $darken-border-dashed-factor); $border-gray-dark: darken($white-normal, $darken-border-factor); /* diff --git a/app/assets/stylesheets/new_nav.scss b/app/assets/stylesheets/new_nav.scss index 2b6c0fc015c..58e205537ef 100644 --- a/app/assets/stylesheets/new_nav.scss +++ b/app/assets/stylesheets/new_nav.scss @@ -9,10 +9,20 @@ header.navbar-gitlab-new { color: $white-light; - background: linear-gradient(to right, $indigo-900, $indigo-800); border-bottom: 0; min-height: $new-navbar-height; + .logo-text { + line-height: initial; + + svg { + width: 55px; + height: 14px; + margin: 0; + fill: $white-light; + } + } + .header-content { display: -webkit-flex; display: flex; @@ -38,10 +48,10 @@ header.navbar-gitlab-new { img { height: 28px; - margin-right: 10px; + margin-right: 8px; } - > a { + a { display: -webkit-flex; display: flex; align-items: center; @@ -54,22 +64,6 @@ header.navbar-gitlab-new { margin-right: 8px; } } - - .logo-text { - line-height: initial; - - svg { - width: 55px; - height: 14px; - margin: 0; - fill: $white-light; - } - } - - &:hover, - &:focus { - background-color: rgba($indigo-200, .2); - } } } @@ -106,7 +100,6 @@ header.navbar-gitlab-new { .navbar-collapse { padding-left: 0; - color: $indigo-200; box-shadow: 0; @media (max-width: $screen-xs-max) { @@ -132,7 +125,6 @@ header.navbar-gitlab-new { font-size: 14px; text-align: center; color: currentColor; - border-left: 1px solid lighten($indigo-700, 10%); &:hover, &:focus, @@ -167,63 +159,49 @@ header.navbar-gitlab-new { will-change: color; margin: 4px 2px; padding: 6px 8px; - color: $indigo-200; height: 32px; @media (max-width: $screen-xs-max) { padding: 0; } - svg { - fill: $indigo-200; - } - &.header-user-dropdown-toggle { margin-left: 2px; .header-user-avatar { - border-color: $indigo-200; margin-right: 0; } } - } - - .header-new-dropdown-toggle { - margin-right: 0; - } - > a:hover, - > a:focus { - text-decoration: none; - outline: 0; - opacity: 1; - color: $white-light; - - @media (min-width: $screen-sm-min) { - background-color: rgba($indigo-200, .2); - } + &:hover, + &:focus { + text-decoration: none; + outline: 0; + opacity: 1; + color: $white-light; - svg { - fill: currentColor; - } + svg { + fill: currentColor; + } - &.header-user-dropdown-toggle { - .header-user-avatar { - border-color: $white-light; + &.header-user-dropdown-toggle { + .header-user-avatar { + border-color: $white-light; + } } } } + .header-new-dropdown-toggle { + margin-right: 0; + } + .impersonated-user, .impersonated-user:hover { margin-right: 1px; background-color: $white-light; border-top-right-radius: 0; border-bottom-right-radius: 0; - - svg { - fill: $indigo-900; - } } .impersonation-btn, @@ -241,8 +219,6 @@ header.navbar-gitlab-new { &.active > a, &.dropdown.open > a { - color: $indigo-900; - background-color: $white-light; svg { fill: currentColor; @@ -256,7 +232,6 @@ header.navbar-gitlab-new { display: -webkit-flex; display: flex; margin: 0 0 0 6px; - color: $indigo-200; .dropdown-chevron { position: relative; @@ -274,17 +249,6 @@ header.navbar-gitlab-new { text-decoration: none; outline: 0; color: $white-light; - background-color: rgba($indigo-200, .2); - - svg { - fill: currentColor; - } - } - - &.active > a, - &.dropdown.open > a { - color: $indigo-900; - background-color: $white-light; svg { fill: currentColor; @@ -309,7 +273,6 @@ header.navbar-gitlab-new { } &.line-separator { - border-left: 1px solid rgba($indigo-200, .2); margin: 8px; } } @@ -339,17 +302,14 @@ header.navbar-gitlab-new { height: 32px; border: 0; border-radius: $border-radius-default; - background-color: rgba($indigo-200, .2); transition: border-color ease-in-out 0.15s, background-color ease-in-out 0.15s; &:hover { - background-color: rgba($indigo-200, .3); box-shadow: none; } } &.search-active form { - background-color: $white-light; box-shadow: none; .search-input { @@ -377,43 +337,26 @@ header.navbar-gitlab-new { } .search-input::placeholder { - color: rgba($indigo-200, .8); transition: color ease-in-out 0.15s; } .location-badge { font-size: 12px; - color: $indigo-100; - background-color: rgba($indigo-200, .1); - will-change: color; margin: -4px 4px -4px -4px; line-height: 25px; padding: 4px 8px; border-radius: 2px 0 0 2px; - border-right: 1px solid $indigo-800; height: 32px; transition: border-color ease-in-out 0.15s; } - .search-input-wrap { - .search-icon, - .clear-icon { - color: rgba($indigo-200, .8); - } - } - &.search-active { .location-badge { - color: $gl-text-color; background-color: $nav-badge-bg; border-color: $border-color; } .search-input-wrap { - .search-icon { - color: rgba($indigo-200, .8); - } - .clear-icon { color: $white-light; } @@ -488,6 +431,7 @@ header.navbar-gitlab-new { .breadcrumb-item-text { @include str-truncated(128px); + text-decoration: inherit; } .breadcrumbs-list-angle { @@ -517,8 +461,6 @@ header.navbar-gitlab-new { .btn-sign-in { margin-top: 3px; - background-color: $indigo-100; - color: $indigo-900; font-weight: $gl-font-weight-bold; &:hover { diff --git a/app/assets/stylesheets/new_sidebar.scss b/app/assets/stylesheets/new_sidebar.scss index 378ef8926d5..8030854e527 100644 --- a/app/assets/stylesheets/new_sidebar.scss +++ b/app/assets/stylesheets/new_sidebar.scss @@ -3,8 +3,6 @@ @import "bootstrap/variables"; $active-background: rgba(0, 0, 0, .04); -$active-border: $indigo-500; -$active-color: $indigo-700; $active-hover-background: $active-background; $active-hover-color: $gl-text-color; $inactive-badge-background: rgba(0, 0, 0, .08); @@ -107,7 +105,8 @@ $new-sidebar-collapsed-width: 50px; } &.sidebar-icons-only { - width: $new-sidebar-collapsed-width; + width: auto; + min-width: $new-sidebar-collapsed-width; .nav-sidebar-inner-scroll { overflow-x: hidden; @@ -126,6 +125,10 @@ $new-sidebar-collapsed-width: 50px; .fly-out-top-item { display: block; } + + .avatar-container { + margin-right: 0; + } } &.nav-sidebar-expanded { @@ -162,16 +165,9 @@ $new-sidebar-collapsed-width: 50px; } li.active { - box-shadow: inset 4px 0 0 $active-border; - > a { - color: $active-color; font-weight: $gl-font-weight-bold; } - - svg { - fill: $active-color; - } } @media (max-width: $screen-xs-max) { @@ -196,7 +192,7 @@ $new-sidebar-collapsed-width: 50px; .nav-sidebar-inner-scroll { height: 100%; width: 100%; - overflow: auto; + overflow: scroll; } .with-performance-bar .nav-sidebar { @@ -224,7 +220,6 @@ $new-sidebar-collapsed-width: 50px; &:hover, &:focus { background: $active-background; - color: $active-color; } } } @@ -258,7 +253,7 @@ $new-sidebar-collapsed-width: 50px; @media (min-width: $screen-sm-min) { position: fixed; top: 0; - left: $new-sidebar-width; + left: 0; min-width: 150px; margin-top: -1px; padding: 4px 1px; @@ -324,7 +319,6 @@ $new-sidebar-collapsed-width: 50px; } .badge { - color: $active-color; font-weight: $gl-font-weight-bold; } @@ -397,10 +391,6 @@ $new-sidebar-collapsed-width: 50px; } .sidebar-sub-level-items { - @media (min-width: $screen-sm-min) { - left: $new-sidebar-collapsed-width; - } - &:not(.flyout-list) { display: none; } @@ -501,13 +491,3 @@ $new-sidebar-collapsed-width: 50px; .with-performance-bar .boards-list { height: calc(100vh - #{$new-navbar-height} - #{$performance-bar-height}); } - - -// Change color of all horizontal tabs to match the new indigo color -.nav-links li.active a { - border-bottom-color: $active-border; - - .badge { - font-weight: $gl-font-weight-bold; - } -} diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index a7acaf6c728..951580ea1fe 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -608,7 +608,7 @@ + .files, + .alert { - margin-top: 30px; + margin-top: 32px; } } } @@ -634,8 +634,16 @@ padding-top: 8px; padding-bottom: 8px; } + + .diff-changed-file { + display: flex; + align-items: center; + } } .diff-file-changes-path { - @include str-truncated(78%); + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index d8a15faf7e9..d01ee4b033c 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -449,6 +449,12 @@ } } } + + .milestone-title span { + @include str-truncated(100%); + display: block; + margin: 0 4px; + } } a { diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss index d4dc43035eb..cf5f933a762 100644 --- a/app/assets/stylesheets/pages/login.scss +++ b/app/assets/stylesheets/pages/login.scss @@ -95,6 +95,8 @@ } .omniauth-container { + font-size: 13px; + p { margin: 0; } diff --git a/app/assets/stylesheets/pages/profiles/preferences.scss b/app/assets/stylesheets/pages/profiles/preferences.scss index 305feaacaa1..c197494b152 100644 --- a/app/assets/stylesheets/pages/profiles/preferences.scss +++ b/app/assets/stylesheets/pages/profiles/preferences.scss @@ -1,3 +1,67 @@ +@mixin application-theme-preview($color-1, $color-2, $color-3, $color-4) { + .one { + background-color: $color-1; + border-top-left-radius: $border-radius-default; + } + + .two { + background-color: $color-2; + border-top-right-radius: $border-radius-default; + } + + .three { + background-color: $color-3; + border-bottom-left-radius: $border-radius-default; + } + + .four { + background-color: $color-4; + border-bottom-right-radius: $border-radius-default; + } +} + +.application-theme { + label { + margin-right: 20px; + text-align: center; + } + + .preview { + font-size: 0; + margin-bottom: 10px; + + &.indigo { + @include application-theme-preview($indigo-900, $indigo-700, $indigo-800, $indigo-500); + } + + &.dark { + @include application-theme-preview($theme-gray-900, $theme-gray-700, $theme-gray-800, $theme-gray-600); + } + + &.light { + @include application-theme-preview($theme-gray-600, $theme-gray-200, $theme-gray-400, $theme-gray-100); + } + + &.blue { + @include application-theme-preview($theme-blue-900, $theme-blue-700, $theme-blue-800, $theme-blue-500); + } + + &.green { + @include application-theme-preview($theme-green-900, $theme-green-700, $theme-green-800, $theme-green-500); + } + } + + .preview-row { + display: block; + } + + .quadrant { + display: inline-block; + height: 50px; + width: 80px; + } +} + .syntax-theme { label { margin-right: 20px; diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 94e4f4334d4..6400b72742c 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -752,7 +752,7 @@ a.deploy-project-label { } li.missing { - border: 1px dashed $border-gray-normal; + border: 1px dashed $border-gray-normal-dashed; border-radius: $border-radius-default; a { diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss index 69abb13add4..7dfcf7b7d9c 100644 --- a/app/assets/stylesheets/pages/repo.scss +++ b/app/assets/stylesheets/pages/repo.scss @@ -71,6 +71,11 @@ height: 100%; .monaco-editor.vs { + .current-line { + border: none; + background: $well-light-border; + } + .line-numbers { cursor: pointer; @@ -84,6 +89,13 @@ } } + .blob-no-preview { + .vertical-center { + justify-content: center; + width: 100%; + } + } + &.edit-mode { .blob-viewer-container { overflow: hidden; @@ -103,7 +115,7 @@ overflow: auto; > div, - .file-content { + .file-content:not(.wiki) { display: flex; } diff --git a/app/controllers/admin/deploy_keys_controller.rb b/app/controllers/admin/deploy_keys_controller.rb index e5cba774dcb..a7ab481519d 100644 --- a/app/controllers/admin/deploy_keys_controller.rb +++ b/app/controllers/admin/deploy_keys_controller.rb @@ -10,9 +10,8 @@ class Admin::DeployKeysController < Admin::ApplicationController end def create - @deploy_key = deploy_keys.new(create_params.merge(user: current_user)) - - if @deploy_key.save + @deploy_key = DeployKeys::CreateService.new(current_user, create_params.merge(public: true)).execute + if @deploy_key.persisted? redirect_to admin_deploy_keys_path else render 'new' diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index 9ec7719fabb..cbcef70e957 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -211,6 +211,7 @@ class Admin::UsersController < Admin::ApplicationController :provider, :remember_me, :skype, + :theme_id, :twitter, :username, :website_url diff --git a/app/controllers/boards/issues_controller.rb b/app/controllers/boards/issues_controller.rb index 8d4ec2d6d9d..0d74078645a 100644 --- a/app/controllers/boards/issues_controller.rb +++ b/app/controllers/boards/issues_controller.rb @@ -11,9 +11,15 @@ module Boards issues = Boards::Issues::ListService.new(board_parent, current_user, filter_params).execute issues = issues.page(params[:page]).per(params[:per] || 20) make_sure_position_is_set(issues) + issues = issues.preload(:project, + :milestone, + :assignees, + labels: [:priorities], + notes: [:award_emoji, :author] + ) render json: { - issues: serialize_as_json(issues.preload(:project)), + issues: serialize_as_json(issues), size: issues.total_count } end @@ -76,14 +82,13 @@ module Boards def serialize_as_json(resource) resource.as_json( - labels: true, only: [:id, :iid, :project_id, :title, :confidential, :due_date, :relative_position], + labels: true, include: { project: { only: [:id, :path] }, assignees: { only: [:id, :name, :username], methods: [:avatar_url] }, milestone: { only: [:id, :title] } - }, - user: current_user + } ) end end diff --git a/app/controllers/ci/lints_controller.rb b/app/controllers/ci/lints_controller.rb index 3eb485de9db..be667687c18 100644 --- a/app/controllers/ci/lints_controller.rb +++ b/app/controllers/ci/lints_controller.rb @@ -7,11 +7,11 @@ module Ci def create @content = params[:content] - @error = Ci::GitlabCiYamlProcessor.validation_message(@content) + @error = Gitlab::Ci::YamlProcessor.validation_message(@content) @status = @error.blank? if @error.blank? - @config_processor = Ci::GitlabCiYamlProcessor.new(@content) + @config_processor = Gitlab::Ci::YamlProcessor.new(@content) @stages = @config_processor.stages @builds = @config_processor.builds @jobs = @config_processor.jobs diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb index f71ab702e71..cd94a36a6e7 100644 --- a/app/controllers/dashboard/projects_controller.rb +++ b/app/controllers/dashboard/projects_controller.rb @@ -48,7 +48,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController ProjectsFinder .new(params: finder_params, current_user: current_user) .execute - .includes(:route, :creator, namespace: :route) + .includes(:route, :creator, namespace: [:route, :owner]) end def load_events diff --git a/app/controllers/profiles/gpg_keys_controller.rb b/app/controllers/profiles/gpg_keys_controller.rb index 6779cc6ddac..689c76059f6 100644 --- a/app/controllers/profiles/gpg_keys_controller.rb +++ b/app/controllers/profiles/gpg_keys_controller.rb @@ -7,9 +7,9 @@ class Profiles::GpgKeysController < Profiles::ApplicationController end def create - @gpg_key = current_user.gpg_keys.new(gpg_key_params) + @gpg_key = GpgKeys::CreateService.new(current_user, gpg_key_params).execute - if @gpg_key.save + if @gpg_key.persisted? redirect_to profile_gpg_keys_path else @gpg_keys = current_user.gpg_keys.select(&:persisted?) diff --git a/app/controllers/profiles/keys_controller.rb b/app/controllers/profiles/keys_controller.rb index f9f0e8eef83..89d6d7f1b52 100644 --- a/app/controllers/profiles/keys_controller.rb +++ b/app/controllers/profiles/keys_controller.rb @@ -11,9 +11,9 @@ class Profiles::KeysController < Profiles::ApplicationController end def create - @key = current_user.keys.new(key_params) + @key = Keys::CreateService.new(current_user, key_params).execute - if @key.save + if @key.persisted? redirect_to profile_key_path(@key) else @keys = current_user.keys.select(&:persisted?) diff --git a/app/controllers/profiles/preferences_controller.rb b/app/controllers/profiles/preferences_controller.rb index 1e557c47638..cce2a847b53 100644 --- a/app/controllers/profiles/preferences_controller.rb +++ b/app/controllers/profiles/preferences_controller.rb @@ -35,7 +35,8 @@ class Profiles::PreferencesController < Profiles::ApplicationController :color_scheme_id, :layout, :dashboard, - :project_view + :project_view, + :theme_id ) end end diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb index 193549663ac..3c8eaa24080 100644 --- a/app/controllers/projects/compare_controller.rb +++ b/app/controllers/projects/compare_controller.rb @@ -27,7 +27,7 @@ class Projects::CompareController < Projects::ApplicationController def create if params[:from].blank? || params[:to].blank? - flash[:alert] = "You must select from and to branches" + flash[:alert] = "You must select a Source and a Target revision" from_to_vars = { from: params[:from].presence, to: params[:to].presence diff --git a/app/controllers/projects/deploy_keys_controller.rb b/app/controllers/projects/deploy_keys_controller.rb index c2e621fa190..cf8829ba95b 100644 --- a/app/controllers/projects/deploy_keys_controller.rb +++ b/app/controllers/projects/deploy_keys_controller.rb @@ -22,7 +22,7 @@ class Projects::DeployKeysController < Projects::ApplicationController end def create - @key = DeployKey.new(create_params.merge(user: current_user)) + @key = DeployKeys::CreateService.new(current_user, create_params).execute unless @key.valid? && @project.deploy_keys << @key flash[:alert] = @key.errors.full_messages.join(', ').html_safe diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index a3bfbf0694e..7ad7b3003af 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -132,10 +132,10 @@ class Projects::PipelinesController < Projects::ApplicationController def charts @charts = {} - @charts[:week] = Ci::Charts::WeekChart.new(project) - @charts[:month] = Ci::Charts::MonthChart.new(project) - @charts[:year] = Ci::Charts::YearChart.new(project) - @charts[:pipeline_times] = Ci::Charts::PipelineTime.new(project) + @charts[:week] = Gitlab::Ci::Charts::WeekChart.new(project) + @charts[:month] = Gitlab::Ci::Charts::MonthChart.new(project) + @charts[:year] = Gitlab::Ci::Charts::YearChart.new(project) + @charts[:pipeline_times] = Gitlab::Ci::Charts::PipelineTime.new(project) @counts = {} @counts[:total] = @project.pipelines.count(:all) diff --git a/app/helpers/auto_devops_helper.rb b/app/helpers/auto_devops_helper.rb index 4ff38f86b5f..c455d18cff8 100644 --- a/app/helpers/auto_devops_helper.rb +++ b/app/helpers/auto_devops_helper.rb @@ -1,7 +1,10 @@ module AutoDevopsHelper def show_auto_devops_callout?(project) - show_callout?('auto_devops_settings_dismissed') && + Feature.get(:auto_devops_banner_disabled).off? && + show_callout?('auto_devops_settings_dismissed') && can?(current_user, :admin_pipeline, project) && - project.has_auto_devops_implicitly_disabled? + project.has_auto_devops_implicitly_disabled? && + !project.repository.gitlab_ci_yml && + project.ci_services.active.none? end end diff --git a/app/helpers/boards_helper.rb b/app/helpers/boards_helper.rb index 4bd61aa8f86..62ac208f16a 100644 --- a/app/helpers/boards_helper.rb +++ b/app/helpers/boards_helper.rb @@ -77,4 +77,8 @@ module BoardsHelper 'max-select': dropdown_options[:data][:'max-select'] } end + + def boards_link_text + _("Board") + end end diff --git a/app/helpers/builds_helper.rb b/app/helpers/builds_helper.rb index 85bc784d53c..aa3a9a055a0 100644 --- a/app/helpers/builds_helper.rb +++ b/app/helpers/builds_helper.rb @@ -30,7 +30,7 @@ module BuildsHelper def build_failed_issue_options { - title: "Build Failed ##{@build.id}", + title: "Job Failed ##{@build.id}", description: project_job_url(@project, @build) } end diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index 36b79da1bde..e8efe8fab27 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -21,7 +21,7 @@ module GroupsHelper group.ancestors.reverse.each_with_index do |parent, index| if index > 0 - add_to_breadcrumb_dropdown(group_title_link(parent, hidable: false, show_avatar: true), location: :before) + add_to_breadcrumb_dropdown(group_title_link(parent, hidable: false, show_avatar: true, for_dropdown: true), location: :before) else full_title += breadcrumb_list_item group_title_link(parent, hidable: false) end @@ -85,8 +85,8 @@ module GroupsHelper private - def group_title_link(group, hidable: false, show_avatar: false) - link_to(group_path(group), class: "group-path breadcrumb-item-text js-breadcrumb-item-text #{'hidable' if hidable}") do + def group_title_link(group, hidable: false, show_avatar: false, for_dropdown: false) + link_to(group_path(group), class: "group-path #{'breadcrumb-item-text' unless for_dropdown} js-breadcrumb-item-text #{'hidable' if hidable}") do output = if (group.try(:avatar_url) || show_avatar) && !Rails.env.test? image_tag(group_icon(group), class: "avatar-tile", width: 15, height: 15) diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 66e1e607e01..df390dd5aab 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -213,7 +213,6 @@ module IssuablesHelper canUpdate: can?(current_user, :update_issue, issuable), canDestroy: can?(current_user, :destroy_issue, issuable), issuableRef: issuable.to_reference, - isConfidential: issuable.confidential, markdownPreviewPath: preview_markdown_path(@project), markdownDocsPath: help_page_path('user/markdown'), issuableTemplates: issuable_templates(issuable), diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb index d36bb4ab074..0d7347ed30d 100644 --- a/app/helpers/preferences_helper.rb +++ b/app/helpers/preferences_helper.rb @@ -40,6 +40,10 @@ module PreferencesHelper ] end + def user_application_theme + @user_application_theme ||= Gitlab::Themes.for_user(current_user).css_class + end + def user_color_scheme Gitlab::ColorSchemes.for_user(current_user).css_class end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index c0114dd0256..ddeff490d3a 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -137,15 +137,7 @@ module ProjectsHelper end def last_push_event - return unless current_user - return current_user.recent_push unless @project - - project_ids = [@project.id] - if fork = current_user.fork_of(@project) - project_ids << fork.id - end - - current_user.recent_push(project_ids) + current_user&.recent_push(@project) end def project_feature_access_select(field) @@ -328,7 +320,7 @@ module ProjectsHelper def git_user_name if current_user - current_user.name + current_user.name.gsub('"', '\"') else _("Your name") end diff --git a/app/helpers/tab_helper.rb b/app/helpers/tab_helper.rb index 3308ab0c259..ee701076a14 100644 --- a/app/helpers/tab_helper.rb +++ b/app/helpers/tab_helper.rb @@ -119,8 +119,4 @@ module TabHelper 'active' if current_controller?('oauth/applications') end - - def sidebar_link(href, title: nil, css: nil, &block) - link_to capture(&block), href, title: (title if collapsed_sidebar?), class: css, aria: { label: title } - end end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 3568e72e463..c0cc60d5ebf 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -137,11 +137,11 @@ class ApplicationSetting < ActiveRecord::Base validates :housekeeping_full_repack_period, presence: true, - numericality: { only_integer: true, greater_than: :housekeeping_incremental_repack_period } + numericality: { only_integer: true, greater_than_or_equal_to: :housekeeping_incremental_repack_period } validates :housekeeping_gc_period, presence: true, - numericality: { only_integer: true, greater_than: :housekeeping_full_repack_period } + numericality: { only_integer: true, greater_than_or_equal_to: :housekeeping_full_repack_period } validates :terminal_max_session_time, presence: true, @@ -247,7 +247,7 @@ class ApplicationSetting < ActiveRecord::Base housekeeping_full_repack_period: 50, housekeeping_gc_period: 200, housekeeping_incremental_repack_period: 10, - import_sources: Gitlab::ImportSources.values, + import_sources: Settings.gitlab['import_sources'], koding_enabled: false, koding_url: nil, max_artifacts_size: Settings.artifacts['max_size'], diff --git a/app/models/blob_viewer/gitlab_ci_yml.rb b/app/models/blob_viewer/gitlab_ci_yml.rb index 7267c3965d3..53bc247dec1 100644 --- a/app/models/blob_viewer/gitlab_ci_yml.rb +++ b/app/models/blob_viewer/gitlab_ci_yml.rb @@ -13,7 +13,7 @@ module BlobViewer prepare! - @validation_message = Ci::GitlabCiYamlProcessor.validation_message(blob.data) + @validation_message = Gitlab::Ci::YamlProcessor.validation_message(blob.data) end def valid? diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 5ebe6f180e6..ee544d8ac56 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -446,8 +446,8 @@ module Ci return unless trace trace = trace.dup - Ci::MaskSecret.mask!(trace, project.runners_token) if project - Ci::MaskSecret.mask!(trace, token) + Gitlab::Ci::MaskSecret.mask!(trace, project.runners_token) if project + Gitlab::Ci::MaskSecret.mask!(trace, token) trace end diff --git a/app/models/ci/group_variable.rb b/app/models/ci/group_variable.rb index f64bc245a67..afeae69ba39 100644 --- a/app/models/ci/group_variable.rb +++ b/app/models/ci/group_variable.rb @@ -1,6 +1,6 @@ module Ci class GroupVariable < ActiveRecord::Base - extend Ci::Model + extend Gitlab::Ci::Model include HasVariable include Presentable diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 871c76fbad3..8d017b9b3b1 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -1,6 +1,6 @@ module Ci class Pipeline < ActiveRecord::Base - extend Ci::Model + extend Gitlab::Ci::Model include HasStatus include Importable include AfterCommitQueue @@ -336,8 +336,8 @@ module Ci return @config_processor if defined?(@config_processor) @config_processor ||= begin - Ci::GitlabCiYamlProcessor.new(ci_yaml_file, project.full_path) - rescue Ci::GitlabCiYamlProcessor::ValidationError, Psych::SyntaxError => e + Gitlab::Ci::YamlProcessor.new(ci_yaml_file, project.full_path) + rescue Gitlab::Ci::YamlProcessor::ValidationError, Psych::SyntaxError => e self.yaml_errors = e.message nil rescue @@ -453,6 +453,10 @@ module Ci .fabricate! end + def latest_builds_with_artifacts + @latest_builds_with_artifacts ||= builds.latest.with_artifacts + end + private def ci_yaml_from_repo diff --git a/app/models/ci/pipeline_schedule.rb b/app/models/ci/pipeline_schedule.rb index e7e02587759..10ead6b6d3b 100644 --- a/app/models/ci/pipeline_schedule.rb +++ b/app/models/ci/pipeline_schedule.rb @@ -1,6 +1,6 @@ module Ci class PipelineSchedule < ActiveRecord::Base - extend Ci::Model + extend Gitlab::Ci::Model include Importable acts_as_paranoid diff --git a/app/models/ci/pipeline_schedule_variable.rb b/app/models/ci/pipeline_schedule_variable.rb index ee5b8733fac..af989fb14b4 100644 --- a/app/models/ci/pipeline_schedule_variable.rb +++ b/app/models/ci/pipeline_schedule_variable.rb @@ -1,6 +1,6 @@ module Ci class PipelineScheduleVariable < ActiveRecord::Base - extend Ci::Model + extend Gitlab::Ci::Model include HasVariable belongs_to :pipeline_schedule diff --git a/app/models/ci/pipeline_variable.rb b/app/models/ci/pipeline_variable.rb index 00b419c3efa..de5aae17a15 100644 --- a/app/models/ci/pipeline_variable.rb +++ b/app/models/ci/pipeline_variable.rb @@ -1,6 +1,6 @@ module Ci class PipelineVariable < ActiveRecord::Base - extend Ci::Model + extend Gitlab::Ci::Model include HasVariable belongs_to :pipeline diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index b1798084787..a0d07902ba2 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -1,6 +1,6 @@ module Ci class Runner < ActiveRecord::Base - extend Ci::Model + extend Gitlab::Ci::Model RUNNER_QUEUE_EXPIRY_TIME = 60.minutes ONLINE_CONTACT_TIMEOUT = 1.hour diff --git a/app/models/ci/runner_project.rb b/app/models/ci/runner_project.rb index 5f01a0daae9..505d178ba8e 100644 --- a/app/models/ci/runner_project.rb +++ b/app/models/ci/runner_project.rb @@ -1,6 +1,6 @@ module Ci class RunnerProject < ActiveRecord::Base - extend Ci::Model + extend Gitlab::Ci::Model belongs_to :runner belongs_to :project diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb index 754c37518b3..75b8ea2a371 100644 --- a/app/models/ci/stage.rb +++ b/app/models/ci/stage.rb @@ -1,6 +1,6 @@ module Ci class Stage < ActiveRecord::Base - extend Ci::Model + extend Gitlab::Ci::Model include Importable include HasStatus include Gitlab::OptimisticLocking diff --git a/app/models/ci/trigger.rb b/app/models/ci/trigger.rb index 6df41a3f301..b5290bcaf53 100644 --- a/app/models/ci/trigger.rb +++ b/app/models/ci/trigger.rb @@ -1,6 +1,6 @@ module Ci class Trigger < ActiveRecord::Base - extend Ci::Model + extend Gitlab::Ci::Model acts_as_paranoid diff --git a/app/models/ci/trigger_request.rb b/app/models/ci/trigger_request.rb index 2c860598281..215b1cf6753 100644 --- a/app/models/ci/trigger_request.rb +++ b/app/models/ci/trigger_request.rb @@ -1,6 +1,6 @@ module Ci class TriggerRequest < ActiveRecord::Base - extend Ci::Model + extend Gitlab::Ci::Model belongs_to :trigger belongs_to :pipeline, foreign_key: :commit_id diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb index cf0fe04ddaf..67d3ec81b6f 100644 --- a/app/models/ci/variable.rb +++ b/app/models/ci/variable.rb @@ -1,6 +1,6 @@ module Ci class Variable < ActiveRecord::Base - extend Ci::Model + extend Gitlab::Ci::Model include HasVariable include Presentable diff --git a/app/models/deploy_key.rb b/app/models/deploy_key.rb index 51768dd96bc..eae5eee4fee 100644 --- a/app/models/deploy_key.rb +++ b/app/models/deploy_key.rb @@ -28,10 +28,4 @@ class DeployKey < Key def can_push_to?(project) can_push? && has_access_to?(project) end - - private - - # we don't want to notify the user for deploy keys - def notify_user - end end diff --git a/app/models/environment.rb b/app/models/environment.rb index 9b05f8b1cd5..44e39e21442 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -6,7 +6,10 @@ class Environment < ActiveRecord::Base belongs_to :project, required: true, validate: true - has_many :deployments, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent + has_many :deployments, + -> (env) { where(project_id: env.project_id) }, + dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent + has_one :last_deployment, -> { order('deployments.id DESC') }, class_name: 'Deployment' before_validation :nullify_external_url diff --git a/app/models/event.rb b/app/models/event.rb index c313bbb66f8..0b1f053a7e6 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -49,7 +49,7 @@ class Event < ActiveRecord::Base belongs_to :author, class_name: "User" belongs_to :project belongs_to :target, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations - has_one :push_event_payload, foreign_key: :event_id + has_one :push_event_payload # Callbacks after_create :reset_project_activity @@ -241,13 +241,7 @@ class Event < ActiveRecord::Base def action_name if push? - if new_ref? - "pushed new" - elsif rm_ref? - "deleted" - else - "pushed to" - end + push_action_name elsif closed? "closed" elsif merged? @@ -263,11 +257,7 @@ class Event < ActiveRecord::Base elsif commented? "commented on" elsif created_project? - if project.external_import? - "imported" - else - "created" - end + created_project_action_name else "opened" end @@ -360,6 +350,24 @@ class Event < ActiveRecord::Base private + def push_action_name + if new_ref? + "pushed new" + elsif rm_ref? + "deleted" + else + "pushed to" + end + end + + def created_project_action_name + if project.external_import? + "imported" + else + "created" + end + end + def recent_update? project.last_activity_at > RESET_PROJECT_ACTIVITY_INTERVAL.ago end diff --git a/app/models/gpg_key.rb b/app/models/gpg_key.rb index 1633acd4fa9..44deae4234b 100644 --- a/app/models/gpg_key.rb +++ b/app/models/gpg_key.rb @@ -36,7 +36,6 @@ class GpgKey < ActiveRecord::Base before_validation :extract_fingerprint, :extract_primary_keyid after_commit :update_invalid_gpg_signatures, on: :create - after_commit :notify_user, on: :create def primary_keyid super&.upcase @@ -107,8 +106,4 @@ class GpgKey < ActiveRecord::Base # only allows one key self.primary_keyid = Gitlab::Gpg.primary_keyids_from_key(key).first end - - def notify_user - NotificationService.new.new_gpg_key(self) - end end diff --git a/app/models/gpg_signature.rb b/app/models/gpg_signature.rb index 454c90d5fc4..1f047a32c84 100644 --- a/app/models/gpg_signature.rb +++ b/app/models/gpg_signature.rb @@ -1,8 +1,5 @@ class GpgSignature < ActiveRecord::Base include ShaAttribute - include IgnorableColumn - - ignore_column :valid_signature sha_attribute :commit_sha sha_attribute :gpg_key_primary_keyid diff --git a/app/models/issue.rb b/app/models/issue.rb index 8c7d492e605..cd5056aae5e 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -30,9 +30,6 @@ class Issue < ActiveRecord::Base has_many :issue_assignees has_many :assignees, class_name: "User", through: :issue_assignees - has_many :issue_assignees - has_many :assignees, class_name: "User", through: :issue_assignees - validates :project, presence: true scope :in_projects, ->(project_ids) { where(project_id: project_ids) } diff --git a/app/models/key.rb b/app/models/key.rb index a6b4dcfec0d..4fa6cac2fd0 100644 --- a/app/models/key.rb +++ b/app/models/key.rb @@ -28,7 +28,6 @@ class Key < ActiveRecord::Base delegate :name, :email, to: :user, prefix: true after_commit :add_to_shell, on: :create - after_commit :notify_user, on: :create after_create :post_create_hook after_commit :remove_from_shell, on: :destroy after_destroy :post_destroy_hook @@ -118,8 +117,4 @@ class Key < ActiveRecord::Base "type is forbidden. Must be #{allowed_types}" end - - def notify_user - NotificationService.new.new_key(self) - end end diff --git a/app/models/label.rb b/app/models/label.rb index 958141a7358..899028a01a0 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -127,7 +127,12 @@ class Label < ActiveRecord::Base end def priority(project) - priorities.find_by(project: project).try(:priority) + priority = if priorities.loaded? + priorities.first { |p| p.project == project } + else + priorities.find_by(project: project) + end + priority.try(:priority) end def template? diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 4a9a23fea1f..e279d8dd8c5 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -231,6 +231,13 @@ class Namespace < ActiveRecord::Base end def force_share_with_group_lock_on_descendants - descendants.update_all(share_with_group_lock: true) + return unless Group.supports_nested_groups? + + # We can't use `descendants.update_all` since Rails will throw away the WITH + # RECURSIVE statement. We also can't use WHERE EXISTS since we can't use + # different table aliases, hence we're just using WHERE IN. Since we have a + # maximum of 20 nested groups this should be fine. + Namespace.where(id: descendants.select(:id)) + .update_all(share_with_group_lock: true) end end diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb index 654be927ed8..ec0ebe4d353 100644 --- a/app/models/personal_access_token.rb +++ b/app/models/personal_access_token.rb @@ -28,7 +28,7 @@ class PersonalAccessToken < ActiveRecord::Base protected def validate_scopes - unless scopes.all? { |scope| Gitlab::Auth::AVAILABLE_SCOPES.include?(scope.to_sym) } + unless revoked || scopes.all? { |scope| Gitlab::Auth::AVAILABLE_SCOPES.include?(scope.to_sym) } errors.add :scopes, "can only contain available scopes" end end diff --git a/app/models/project.rb b/app/models/project.rb index ff5638dd155..94ae0acbe1a 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -161,7 +161,7 @@ class Project < ActiveRecord::Base has_many :notification_settings, as: :source, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent has_one :import_data, class_name: 'ProjectImportData', inverse_of: :project, autosave: true - has_one :project_feature + has_one :project_feature, inverse_of: :project has_one :statistics, class_name: 'ProjectStatistics' # Container repositories need to remove data from the container registry, @@ -190,7 +190,7 @@ class Project < ActiveRecord::Base has_one :auto_devops, class_name: 'ProjectAutoDevops' accepts_nested_attributes_for :variables, allow_destroy: true - accepts_nested_attributes_for :project_feature + accepts_nested_attributes_for :project_feature, update_only: true accepts_nested_attributes_for :import_data accepts_nested_attributes_for :auto_devops @@ -1163,6 +1163,23 @@ class Project < ActiveRecord::Base pipelines.order(id: :desc).find_by(sha: sha, ref: ref) end + def latest_successful_pipeline_for_default_branch + if defined?(@latest_successful_pipeline_for_default_branch) + return @latest_successful_pipeline_for_default_branch + end + + @latest_successful_pipeline_for_default_branch = + pipelines.latest_successful_for(default_branch) + end + + def latest_successful_pipeline_for(ref = nil) + if ref && ref != default_branch + pipelines.latest_successful_for(ref) + else + latest_successful_pipeline_for_default_branch + end + end + def enable_ci project_feature.update_attribute(:builds_access_level, ProjectFeature::ENABLED) end diff --git a/app/models/project_auto_devops.rb b/app/models/project_auto_devops.rb index 53731579e87..7af3b6870e2 100644 --- a/app/models/project_auto_devops.rb +++ b/app/models/project_auto_devops.rb @@ -1,6 +1,9 @@ class ProjectAutoDevops < ActiveRecord::Base belongs_to :project + scope :enabled, -> { where(enabled: true) } + scope :disabled, -> { where(enabled: false) } + validates :domain, allow_blank: true, hostname: { allow_numeric_hostname: true } def variables diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb index fb1db0255aa..bfb8d703ec9 100644 --- a/app/models/project_feature.rb +++ b/app/models/project_feature.rb @@ -41,6 +41,8 @@ class ProjectFeature < ActiveRecord::Base # http://stackoverflow.com/questions/1540645/how-to-disable-default-scope-for-a-belongs-to belongs_to :project, -> { unscope(where: :pending_delete) } + validates :project, presence: true + validate :repository_children_level default_value_for :builds_access_level, value: ENABLED, allows_nil: false diff --git a/app/models/project_services/pipelines_email_service.rb b/app/models/project_services/pipelines_email_service.rb index 9d37184be2c..6a3118a11b8 100644 --- a/app/models/project_services/pipelines_email_service.rb +++ b/app/models/project_services/pipelines_email_service.rb @@ -80,6 +80,6 @@ class PipelinesEmailService < Service end def retrieve_recipients(data) - recipients.to_s.split(',').reject(&:blank?) + recipients.to_s.split(/[,(?:\r?\n) ]+/).reject(&:empty?) end end diff --git a/app/models/push_event.rb b/app/models/push_event.rb index 23ffb0d4ea8..708513c7861 100644 --- a/app/models/push_event.rb +++ b/app/models/push_event.rb @@ -30,6 +30,44 @@ class PushEvent < Event delegate :commit_count, to: :push_event_payload alias_method :commits_count, :commit_count + # Returns events of pushes that either pushed to an existing ref or created a + # new one. + def self.created_or_pushed + actions = [ + PushEventPayload.actions[:pushed], + PushEventPayload.actions[:created] + ] + + joins(:push_event_payload) + .where(push_event_payloads: { action: actions }) + end + + # Returns events of pushes to a branch. + def self.branch_events + ref_type = PushEventPayload.ref_types[:branch] + + joins(:push_event_payload) + .where(push_event_payloads: { ref_type: ref_type }) + end + + # Returns PushEvent instances for which no merge requests have been created. + def self.without_existing_merge_requests + existing_mrs = MergeRequest.except(:order) + .select(1) + .where('merge_requests.source_project_id = events.project_id') + .where('merge_requests.source_branch = push_event_payloads.ref') + + # For reasons unknown the use of #eager_load will result in the + # "push_event_payload" association not being set. Because of this we're + # using "joins" here, which does mean an additional query needs to be + # executed in order to retrieve the "push_event_association" when the + # returned PushEvent is used. + joins(:push_event_payload) + .where('NOT EXISTS (?)', existing_mrs) + .created_or_pushed + .branch_events + end + def self.sti_name PUSHED end diff --git a/app/models/repository.rb b/app/models/repository.rb index 035f85a0b46..f2b54705e7b 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -90,6 +90,12 @@ class Repository ) end + # we need to have this method here because it is not cached in ::Git and + # the method is called multiple times for every request + def has_visible_content? + branch_count > 0 + end + def inspect "#<#{self.class.name}:#{@disk_path}>" end @@ -166,7 +172,7 @@ class Repository end def add_branch(user, branch_name, ref) - branch = raw_repository.add_branch(branch_name, committer: user, target: ref) + branch = raw_repository.add_branch(branch_name, user: user, target: ref) after_create_branch @@ -176,7 +182,7 @@ class Repository end def add_tag(user, tag_name, target, message = nil) - raw_repository.add_tag(tag_name, committer: user, target: target, message: message) + raw_repository.add_tag(tag_name, user: user, target: target, message: message) rescue Gitlab::Git::Repository::InvalidRef false end @@ -184,7 +190,7 @@ class Repository def rm_branch(user, branch_name) before_remove_branch - raw_repository.rm_branch(branch_name, committer: user) + raw_repository.rm_branch(branch_name, user: user) after_remove_branch true @@ -193,7 +199,7 @@ class Repository def rm_tag(user, tag_name) before_remove_tag - raw_repository.rm_tag(tag_name, committer: user) + raw_repository.rm_tag(tag_name, user: user) after_remove_tag true diff --git a/app/models/user.rb b/app/models/user.rb index 26b14ade5ca..09c9b3250eb 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -35,6 +35,7 @@ class User < ActiveRecord::Base default_value_for :project_view, :files default_value_for :notified_of_own_activity, false default_value_for :preferred_language, I18n.default_locale + default_value_for :theme_id, gitlab_config.default_theme attr_encrypted :otp_secret, key: Gitlab::Application.secrets.otp_key_base, @@ -650,20 +651,13 @@ class User < ActiveRecord::Base @personal_projects_count ||= personal_projects.count end - def recent_push(project_ids = nil) - # Get push events not earlier than 2 hours ago - events = recent_events.code_push.where("created_at > ?", Time.now - 2.hours) - events = events.where(project_id: project_ids) if project_ids + def recent_push(project = nil) + service = Users::LastPushEventService.new(self) - # Use the latest event that has not been pushed or merged recently - events.includes(:project).recent.find do |event| - next unless event.project.repository.branch_exists?(event.branch_name) - - merge_requests = MergeRequest.where("created_at >= ?", event.created_at) - .where(source_project_id: event.project.id, - source_branch: event.branch_name) - - merge_requests.empty? + if project + service.last_event_for_project(project) + else + service.last_event_for_user end end diff --git a/app/serializers/build_details_entity.rb b/app/serializers/build_details_entity.rb index 743a08acefe..8c89eea607f 100644 --- a/app/serializers/build_details_entity.rb +++ b/app/serializers/build_details_entity.rb @@ -32,8 +32,8 @@ class BuildDetailsEntity < JobEntity private def build_failed_issue_options - { title: "Build Failed ##{build.id}", - description: project_job_path(project, build) } + { title: "Job Failed ##{build.id}", + description: "Job [##{build.id}](#{project_job_path(project, build)}) failed for #{build.sha}:\n" } end def current_user diff --git a/app/services/ci/pipeline_trigger_service.rb b/app/services/ci/pipeline_trigger_service.rb index 1e5ad28ba57..120af8c1e61 100644 --- a/app/services/ci/pipeline_trigger_service.rb +++ b/app/services/ci/pipeline_trigger_service.rb @@ -14,7 +14,7 @@ module Ci pipeline = Ci::CreatePipelineService.new(project, trigger.owner, ref: params[:ref]) .execute(:trigger, ignore_skip_ci: true) do |pipeline| - trigger.trigger_requests.create!(pipeline: pipeline) + pipeline.trigger_requests.create!(trigger: trigger) create_pipeline_variables!(pipeline) end diff --git a/app/services/deploy_keys/create_service.rb b/app/services/deploy_keys/create_service.rb new file mode 100644 index 00000000000..16de3d08df2 --- /dev/null +++ b/app/services/deploy_keys/create_service.rb @@ -0,0 +1,7 @@ +module DeployKeys + class CreateService < Keys::BaseService + def execute + DeployKey.create(params.merge(user: user)) + end + end +end diff --git a/app/services/event_create_service.rb b/app/services/event_create_service.rb index 0b7e4f187f7..6328d567a07 100644 --- a/app/services/event_create_service.rb +++ b/app/services/event_create_service.rb @@ -74,12 +74,19 @@ class EventCreateService # We're using an explicit transaction here so that any errors that may occur # when creating push payload data will result in the event creation being # rolled back as well. - Event.transaction do - event = create_event(project, current_user, Event::PUSHED) + event = Event.transaction do + new_event = create_event(project, current_user, Event::PUSHED) - PushEventPayloadService.new(event, push_data).execute + PushEventPayloadService + .new(new_event, push_data) + .execute + + new_event end + Users::LastPushEventService.new(current_user) + .cache_last_push_event(event) + Users::ActivityService.new(current_user, 'push').execute end diff --git a/app/services/gpg_keys/create_service.rb b/app/services/gpg_keys/create_service.rb new file mode 100644 index 00000000000..e822a89c4d3 --- /dev/null +++ b/app/services/gpg_keys/create_service.rb @@ -0,0 +1,9 @@ +module GpgKeys + class CreateService < Keys::BaseService + def execute + key = user.gpg_keys.create(params) + notification_service.new_gpg_key(key) if key.persisted? + key + end + end +end diff --git a/app/services/keys/base_service.rb b/app/services/keys/base_service.rb new file mode 100644 index 00000000000..545832d0bd4 --- /dev/null +++ b/app/services/keys/base_service.rb @@ -0,0 +1,13 @@ +module Keys + class BaseService + attr_accessor :user, :params + + def initialize(user, params) + @user, @params = user, params + end + + def notification_service + NotificationService.new + end + end +end diff --git a/app/services/keys/create_service.rb b/app/services/keys/create_service.rb new file mode 100644 index 00000000000..e2e5a6c46c5 --- /dev/null +++ b/app/services/keys/create_service.rb @@ -0,0 +1,9 @@ +module Keys + class CreateService < ::Keys::BaseService + def execute + key = user.keys.create(params) + notification_service.new_key(key) if key.persisted? + key + end + end +end diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb index cb4ffcab778..13e292a18bf 100644 --- a/app/services/projects/update_service.rb +++ b/app/services/projects/update_service.rb @@ -24,7 +24,10 @@ module Projects success else - error('Project could not be updated!') + model_errors = project.errors.full_messages.to_sentence + error_message = model_errors.presence || 'Project could not be updated!' + + error(error_message) end end diff --git a/app/services/users/last_push_event_service.rb b/app/services/users/last_push_event_service.rb new file mode 100644 index 00000000000..f2bfb60604f --- /dev/null +++ b/app/services/users/last_push_event_service.rb @@ -0,0 +1,83 @@ +module Users + # Service class for caching and retrieving the last push event of a user. + class LastPushEventService + EXPIRATION = 2.hours + + def initialize(user) + @user = user + end + + # Caches the given push event for the current user in the Rails cache. + # + # event - An instance of PushEvent to cache. + def cache_last_push_event(event) + keys = [ + project_cache_key(event.project), + user_cache_key + ] + + if event.project.forked? + keys << project_cache_key(event.project.forked_from_project) + end + + keys.each { |key| set_key(key, event.id) } + end + + # Returns the last PushEvent for the current user. + # + # This method will return nil if no event was found. + def last_event_for_user + find_cached_event(user_cache_key) + end + + # Returns the last PushEvent for the current user and the given project. + # + # project - An instance of Project for which to retrieve the PushEvent. + # + # This method will return nil if no event was found. + def last_event_for_project(project) + find_cached_event(project_cache_key(project)) + end + + def find_cached_event(cache_key) + event_id = get_key(cache_key) + + return unless event_id + + unless (event = find_event_in_database(event_id)) + # We don't want to keep querying the same data over and over when a + # merge request has been created, thus we remove the key if no event + # (meaning an MR was created) is returned. + Rails.cache.delete(cache_key) + end + + event + end + + private + + def find_event_in_database(id) + PushEvent + .without_existing_merge_requests + .find_by(id: id) + end + + def user_cache_key + "last-push-event/#{@user.id}" + end + + def project_cache_key(project) + "last-push-event/#{@user.id}/#{project.id}" + end + + def get_key(key) + Rails.cache.read(key, raw: true) + end + + def set_key(key, value) + # We're using raw values here since this takes up less space and we don't + # store complex objects. + Rails.cache.write(key, value, raw: true, expires_in: EXPIRATION) + end + end +end diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml index 069f8f89e0b..703f4165128 100644 --- a/app/views/admin/dashboard/index.html.haml +++ b/app/views/admin/dashboard/index.html.haml @@ -111,6 +111,11 @@ GitLab API %span.pull-right = API::API::version + - if Gitlab.config.pages.enabled + %p + GitLab Pages + %span.pull-right + = Gitlab::Pages::VERSION %p Git %span.pull-right diff --git a/app/views/devise/shared/_omniauth_box.html.haml b/app/views/devise/shared/_omniauth_box.html.haml index bfd7dd25a7d..546cec4d565 100644 --- a/app/views/devise/shared/_omniauth_box.html.haml +++ b/app/views/devise/shared/_omniauth_box.html.haml @@ -7,6 +7,8 @@ %span.light - has_icon = provider_has_icon?(provider) = 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.prepend-top-10 - = check_box_tag :remember_me - = label_tag :remember_me, 'Remember me' + %fieldset.prepend-top-10.checkbox.remember-me + %label + = check_box_tag :remember_me, nil, false, class: 'remember-me-checkbox' + %span + Remember me diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 65ac8aaa59b..0ca34b276a7 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -1,7 +1,7 @@ !!! 5 %html{ lang: I18n.locale, class: page_class } = render "layouts/head" - %body{ class: @body_class, data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}", find_file: find_file_path } } + %body{ class: "#{user_application_theme} #{@body_class}", data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}", find_file: find_file_path } } = render "layouts/init_auto_complete" if @gfm_form = render 'peek/bar' = render "layouts/header/default" diff --git a/app/views/layouts/nav/sidebar/_admin.html.haml b/app/views/layouts/nav/sidebar/_admin.html.haml index 8ab2b686f86..615238b94ad 100644 --- a/app/views/layouts/nav/sidebar/_admin.html.haml +++ b/app/views/layouts/nav/sidebar/_admin.html.haml @@ -6,15 +6,15 @@ = icon('wrench') .sidebar-context-title Admin Area %ul.sidebar-top-level-items - = nav_link(controller: %w(dashboard admin projects users groups jobs runners cohorts), html_options: {class: 'home'}) do - = sidebar_link admin_root_path, title: _('Overview'), css: 'shortcuts-tree' do + = nav_link(controller: %w(dashboard admin projects users groups jobs runners cohorts conversational_development_index), html_options: {class: 'home'}) do + = link_to admin_root_path, class: 'shortcuts-tree' do .nav-icon-container = custom_icon('overview') %span.nav-item-name Overview %ul.sidebar-sub-level-items - = nav_link(controller: %w(dashboard admin projects users groups jobs runners cohorts), html_options: { class: "fly-out-top-item" } ) do + = nav_link(controller: %w(dashboard admin projects users groups jobs runners cohorts conversational_development_index), html_options: { class: "fly-out-top-item" } ) do = link_to admin_root_path do %strong.fly-out-top-item-name #{ _('Overview') } @@ -52,16 +52,16 @@ %span ConvDev Index - = nav_link(controller: %w(conversational_development_index system_info background_jobs logs health_check requests_profiles)) do - = sidebar_link admin_conversational_development_index_path, title: _('Monitoring') do + = nav_link(controller: %w(system_info background_jobs logs health_check requests_profiles)) do + = link_to admin_system_info_path do .nav-icon-container = custom_icon('monitoring') %span.nav-item-name Monitoring %ul.sidebar-sub-level-items - = nav_link(controller: %w(conversational_development_index system_info background_jobs logs health_check requests_profiles), html_options: { class: "fly-out-top-item" } ) do - = link_to admin_conversational_development_index_path do + = nav_link(controller: %w(system_info background_jobs logs health_check requests_profiles), html_options: { class: "fly-out-top-item" } ) do + = link_to admin_system_info_path do %strong.fly-out-top-item-name #{ _('Monitoring') } %li.divider.fly-out-top-item @@ -87,7 +87,7 @@ Requests Profiles = nav_link(controller: :broadcast_messages) do - = sidebar_link admin_broadcast_messages_path, title: _('Messages') do + = link_to admin_broadcast_messages_path do .nav-icon-container = custom_icon('messages') %span.nav-item-name @@ -99,7 +99,7 @@ #{ _('Messages') } = nav_link(controller: [:hooks, :hook_logs]) do - = sidebar_link admin_hooks_path, title: _('Hooks') do + = link_to admin_hooks_path do .nav-icon-container = custom_icon('system_hooks') %span.nav-item-name @@ -111,7 +111,7 @@ #{ _('System Hooks') } = nav_link(controller: :applications) do - = sidebar_link admin_applications_path, title: _('Applications') do + = link_to admin_applications_path do .nav-icon-container = custom_icon('applications') %span.nav-item-name @@ -123,7 +123,7 @@ #{ _('Applications') } = nav_link(controller: :abuse_reports) do - = sidebar_link admin_abuse_reports_path, title: _("Abuse Reports") do + = link_to admin_abuse_reports_path do .nav-icon-container = custom_icon('abuse_reports') %span.nav-item-name @@ -138,7 +138,7 @@ - if akismet_enabled? = nav_link(controller: :spam_logs) do - = sidebar_link admin_spam_logs_path, title: _("Spam Logs") do + = link_to admin_spam_logs_path do .nav-icon-container = custom_icon('spam_logs') %span.nav-item-name @@ -150,7 +150,7 @@ #{ _('Spam Logs') } = nav_link(controller: :deploy_keys) do - = sidebar_link admin_deploy_keys_path, title: _('Deploy Keys') do + = link_to admin_deploy_keys_path do .nav-icon-container = custom_icon('key') %span.nav-item-name @@ -162,7 +162,7 @@ #{ _('Deploy Keys') } = nav_link(controller: :services) do - = sidebar_link admin_application_settings_services_path, title: _('Service Templates') do + = link_to admin_application_settings_services_path do .nav-icon-container = custom_icon('service_templates') %span.nav-item-name @@ -174,7 +174,7 @@ #{ _('Service Templates') } = nav_link(controller: :labels) do - = sidebar_link admin_labels_path, title: _('Labels') do + = link_to admin_labels_path do .nav-icon-container = custom_icon('labels') %span.nav-item-name @@ -186,7 +186,7 @@ #{ _('Labels') } = nav_link(controller: :appearances) do - = sidebar_link admin_appearances_path, title: _('Appearances') do + = link_to admin_appearances_path do .nav-icon-container = custom_icon('appearance') %span.nav-item-name @@ -198,7 +198,7 @@ #{ _('Appearance') } = nav_link(controller: :application_settings) do - = sidebar_link admin_application_settings_path, title: _('Settings') do + = link_to admin_application_settings_path do .nav-icon-container = custom_icon('settings') %span.nav-item-name diff --git a/app/views/layouts/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml index e01dfa7c854..910e0d8b5ec 100644 --- a/app/views/layouts/nav/sidebar/_group.html.haml +++ b/app/views/layouts/nav/sidebar/_group.html.haml @@ -11,7 +11,7 @@ = @group.name %ul.sidebar-top-level-items = nav_link(path: ['groups#show', 'groups#activity', 'groups#subgroups'], html_options: { class: 'home' }) do - = sidebar_link group_path(@group), title: _('Group overview') do + = link_to group_path(@group) do .nav-icon-container = custom_icon('project') %span.nav-item-name @@ -34,7 +34,7 @@ Activity = nav_link(path: ['groups#issues', 'labels#index', 'milestones#index']) do - = sidebar_link issues_group_path(@group), title: _('Issues') do + = link_to issues_group_path(@group) do .nav-icon-container = custom_icon('issues') %span.nav-item-name @@ -64,7 +64,7 @@ Milestones = nav_link(path: 'groups#merge_requests') do - = sidebar_link merge_requests_group_path(@group), title: _('Merge Requests') do + = link_to merge_requests_group_path(@group) do .nav-icon-container = custom_icon('mr_bold') %span.nav-item-name @@ -77,7 +77,7 @@ #{ _('Merge Requests') } %span.badge.count.merge_counter.js-merge-counter.fly-out-badge= number_with_delimiter(merge_requests.count) = nav_link(path: 'group_members#index') do - = sidebar_link group_group_members_path(@group), title: _('Members') do + = link_to group_group_members_path(@group) do .nav-icon-container = custom_icon('members') %span.nav-item-name @@ -89,7 +89,7 @@ #{ _('Members') } - if current_user && can?(current_user, :admin_group, @group) = nav_link(path: %w[groups#projects groups#edit ci_cd#show]) do - = sidebar_link edit_group_path(@group), title: _('Settings') do + = link_to edit_group_path(@group) do .nav-icon-container = custom_icon('settings') %span.nav-item-name diff --git a/app/views/layouts/nav/sidebar/_profile.html.haml b/app/views/layouts/nav/sidebar/_profile.html.haml index 4c26d107ea7..2c402591f62 100644 --- a/app/views/layouts/nav/sidebar/_profile.html.haml +++ b/app/views/layouts/nav/sidebar/_profile.html.haml @@ -7,7 +7,7 @@ .sidebar-context-title User Settings %ul.sidebar-top-level-items = nav_link(path: 'profiles#show', html_options: {class: 'home'}) do - = sidebar_link profile_path, title: _('Profile Settings') do + = link_to profile_path do .nav-icon-container = custom_icon('profile') %span.nav-item-name @@ -18,7 +18,7 @@ %strong.fly-out-top-item-name #{ _('Profile') } = nav_link(controller: [:accounts, :two_factor_auths]) do - = sidebar_link profile_account_path, title: _('Account') do + = link_to profile_account_path do .nav-icon-container = custom_icon('account') %span.nav-item-name @@ -30,7 +30,7 @@ #{ _('Account') } - if current_application_settings.user_oauth_applications? = nav_link(controller: 'oauth/applications') do - = sidebar_link applications_profile_path, title: _('Applications') do + = link_to applications_profile_path do .nav-icon-container = custom_icon('applications') %span.nav-item-name @@ -41,7 +41,7 @@ %strong.fly-out-top-item-name #{ _('Applications') } = nav_link(controller: :chat_names) do - = sidebar_link profile_chat_names_path, title: _('Chat') do + = link_to profile_chat_names_path do .nav-icon-container = custom_icon('chat') %span.nav-item-name @@ -52,7 +52,7 @@ %strong.fly-out-top-item-name #{ _('Chat') } = nav_link(controller: :personal_access_tokens) do - = sidebar_link profile_personal_access_tokens_path, title: _('Access Tokens') do + = link_to profile_personal_access_tokens_path do .nav-icon-container = custom_icon('access_tokens') %span.nav-item-name @@ -63,7 +63,7 @@ %strong.fly-out-top-item-name #{ _('Access Tokens') } = nav_link(controller: :emails) do - = sidebar_link profile_emails_path, title: _('Emails') do + = link_to profile_emails_path do .nav-icon-container = custom_icon('emails') %span.nav-item-name @@ -75,7 +75,7 @@ #{ _('Emails') } - unless current_user.ldap_user? = nav_link(controller: :passwords) do - = sidebar_link edit_profile_password_path, title: _('Password') do + = link_to edit_profile_password_path do .nav-icon-container = custom_icon('lock') %span.nav-item-name @@ -86,7 +86,7 @@ %strong.fly-out-top-item-name #{ _('Password') } = nav_link(controller: :notifications) do - = sidebar_link profile_notifications_path, title: _('Notifications') do + = link_to profile_notifications_path do .nav-icon-container = custom_icon('notifications') %span.nav-item-name @@ -97,7 +97,7 @@ %strong.fly-out-top-item-name #{ _('Notifications') } = nav_link(controller: :keys) do - = sidebar_link profile_keys_path, title: _('SSH Keys') do + = link_to profile_keys_path do .nav-icon-container = custom_icon('key') %span.nav-item-name @@ -108,7 +108,7 @@ %strong.fly-out-top-item-name #{ _('SSH Keys') } = nav_link(controller: :gpg_keys) do - = sidebar_link profile_gpg_keys_path, title: _('GPG Keys') do + = link_to profile_gpg_keys_path do .nav-icon-container = custom_icon('key_2') %span.nav-item-name @@ -119,7 +119,7 @@ %strong.fly-out-top-item-name #{ _('GPG Keys') } = nav_link(controller: :preferences) do - = sidebar_link profile_preferences_path, title: _('Preferences') do + = link_to profile_preferences_path do .nav-icon-container = custom_icon('preferences') %span.nav-item-name @@ -130,7 +130,7 @@ %strong.fly-out-top-item-name #{ _('Preferences') } = nav_link(path: 'profiles#audit_log') do - = sidebar_link audit_log_profile_path, title: _('Authentication log') do + = link_to audit_log_profile_path do .nav-icon-container = custom_icon('authentication_log') %span.nav-item-name diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml index 9589e81c750..29f1fc6b354 100644 --- a/app/views/layouts/nav/sidebar/_project.html.haml +++ b/app/views/layouts/nav/sidebar/_project.html.haml @@ -9,7 +9,7 @@ = @project.name %ul.sidebar-top-level-items = nav_link(path: ['projects#show', 'projects#activity', 'cycle_analytics#show'], html_options: { class: 'home' }) do - = sidebar_link project_path(@project), title: _('Project overview'), css: 'shortcuts-project' do + = link_to project_path(@project), class: 'shortcuts-project' do .nav-icon-container = custom_icon('project') %span.nav-item-name @@ -36,7 +36,7 @@ - if project_nav_tab? :files = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file commit commits compare projects/repositories tags branches releases graphs network)) do - = sidebar_link project_tree_path(@project), title: _('Repository'), css: 'shortcuts-tree' do + = link_to project_tree_path(@project), class: 'shortcuts-tree' do .nav-icon-container = custom_icon('doc_text') %span.nav-item-name @@ -82,7 +82,7 @@ - if project_nav_tab? :container_registry = nav_link(controller: %w[projects/registry/repositories]) do - = sidebar_link project_container_registry_index_path(@project), title: _('Container Registry'), css: 'shortcuts-container-registry' do + = link_to project_container_registry_index_path(@project), class: 'shortcuts-container-registry' do .nav-icon-container = custom_icon('container_registry') %span.nav-item-name @@ -90,7 +90,7 @@ - if project_nav_tab? :issues = nav_link(controller: @project.issues_enabled? ? [:issues, :labels, :milestones, :boards] : :issues) do - = sidebar_link project_issues_path(@project), title: _('Issues'), css: 'shortcuts-issues' do + = link_to project_issues_path(@project), class: 'shortcuts-issues' do .nav-icon-container = custom_icon('issues') %span.nav-item-name @@ -114,9 +114,9 @@ List = nav_link(controller: :boards) do - = link_to project_boards_path(@project), title: 'Board' do + = link_to project_boards_path(@project), title: boards_link_text do %span - Board + = boards_link_text .feature-highlight.js-feature-highlight{ disabled: true, data: { trigger: 'manual', container: 'body', toggle: 'popover', placement: 'right', highlight: 'issue-boards' } } .feature-highlight-popover-content = render 'feature_highlight/issue_boards.svg' @@ -144,7 +144,7 @@ - if project_nav_tab? :merge_requests = nav_link(controller: @project.issues_enabled? ? :merge_requests : [:merge_requests, :labels, :milestones]) do - = sidebar_link project_merge_requests_path(@project), title: _('Merge Requests'), css: 'shortcuts-merge_requests' do + = link_to project_merge_requests_path(@project), class: 'shortcuts-merge_requests' do .nav-icon-container = custom_icon('mr_bold') %span.nav-item-name @@ -161,7 +161,7 @@ - if project_nav_tab? :pipelines = nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :environments, :artifacts]) do - = sidebar_link project_pipelines_path(@project), title: _('CI / CD'), css: 'shortcuts-pipelines' do + = link_to project_pipelines_path(@project), class: 'shortcuts-pipelines' do .nav-icon-container = custom_icon('pipeline') %span.nav-item-name @@ -205,7 +205,7 @@ - if project_nav_tab? :wiki = nav_link(controller: :wikis) do - = sidebar_link get_project_wiki_path(@project), title: _('Wiki'), css: 'shortcuts-wiki' do + = link_to get_project_wiki_path(@project), class: 'shortcuts-wiki' do .nav-icon-container = custom_icon('wiki') %span.nav-item-name @@ -218,7 +218,7 @@ - if project_nav_tab? :snippets = nav_link(controller: :snippets) do - = sidebar_link project_snippets_path(@project), title: _('Snippets'), css: 'shortcuts-snippets' do + = link_to project_snippets_path(@project), class: 'shortcuts-snippets' do .nav-icon-container = custom_icon('snippets') %span.nav-item-name @@ -231,7 +231,7 @@ - if project_nav_tab? :settings = nav_link(path: %w[projects#edit project_members#index integrations#show services#edit repository#show ci_cd#show pages#show]) do - = sidebar_link edit_project_path(@project), title: _('Settings'), css: 'shortcuts-tree' do + = link_to edit_project_path(@project), class: 'shortcuts-tree' do .nav-icon-container = custom_icon('settings') %span.nav-item-name diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml index 9e7fe556d88..66d1d1e8d44 100644 --- a/app/views/profiles/preferences/show.html.haml +++ b/app/views/profiles/preferences/show.html.haml @@ -3,6 +3,26 @@ = render 'profiles/head' = form_for @user, url: profile_preferences_path, remote: true, method: :put, html: { class: 'row prepend-top-default js-preferences-form' } do |f| + .col-lg-4.application-theme + %h4.prepend-top-0 + GitLab navigation theme + %p Customize the appearance of the application header and navigation sidebar. + .col-lg-8.application-theme + - Gitlab::Themes.each do |theme| + = label_tag do + .preview{ class: theme.name.downcase } + .preview-row + .quadrant.one + .quadrant.two + .preview-row + .quadrant.three + .quadrant.four + = f.radio_button :theme_id, theme.id, checked: Gitlab::Themes.for_user(@user).id == theme.id + = theme.name + + .col-sm-12 + %hr + .col-lg-4.profile-settings-sidebar %h4.prepend-top-0 Syntax highlighting theme @@ -16,10 +36,10 @@ .preview= image_tag "#{scheme.css_class}-scheme-preview.png" = f.radio_button :color_scheme_id, scheme.id = scheme.name + .col-sm-12 %hr - .col-sm-12 - %hr + .col-lg-4.profile-settings-sidebar %h4.prepend-top-0 Behavior diff --git a/app/views/profiles/preferences/update.js.erb b/app/views/profiles/preferences/update.js.erb index 431ab9d052b..8966dd3fd86 100644 --- a/app/views/profiles/preferences/update.js.erb +++ b/app/views/profiles/preferences/update.js.erb @@ -1,3 +1,7 @@ +// Remove body class for any previous theme, re-add current one +$('body').removeClass('<%= Gitlab::Themes.body_classes %>') +$('body').addClass('<%= user_application_theme %>') + // Toggle container-fluid class if ('<%= current_user.layout %>' === 'fluid') { $('.content-wrapper .container-fluid').removeClass('container-limited') diff --git a/app/views/projects/blob/viewers/_download.html.haml b/app/views/projects/blob/viewers/_download.html.haml index 6d1138f7959..253566c43be 100644 --- a/app/views/projects/blob/viewers/_download.html.haml +++ b/app/views/projects/blob/viewers/_download.html.haml @@ -1,5 +1,5 @@ .file-content.blob_file.blob-no-preview - .center + .center.render-error.vertical-center = link_to blob_raw_path do %h1.light = icon('download') diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index 19712a8f1be..05c1d2b383c 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -43,7 +43,8 @@ data: { toggle: "modal", target: "#modal-delete-branch", delete_path: project_branch_path(@project, branch.name), - branch_name: branch.name } } + branch_name: branch.name, + is_merged: ("true" if @repository.merged_to_root_ref?(branch.name)) } } = icon("trash-o") - else %button{ class: "btn btn-remove remove-row js-ajax-loading-spinner has-tooltip disabled", diff --git a/app/views/projects/branches/_delete_protected_modal.html.haml b/app/views/projects/branches/_delete_protected_modal.html.haml index c5888afa54d..f00a0ee6925 100644 --- a/app/views/projects/branches/_delete_protected_modal.html.haml +++ b/app/views/projects/branches/_delete_protected_modal.html.haml @@ -6,13 +6,18 @@ %h3.page-title Delete protected branch = surround "'", "'?" do - %span.js-branch-name>[branch name] + %span.js-branch-name.ref-name>[branch name] .modal-body %p You’re about to permanently delete the protected branch = succeed '.' do - %strong.js-branch-name [branch name] + %strong.js-branch-name.ref-name [branch name] + %p.js-not-merged + - default_branch = capture do + %span.ref-name= @repository.root_ref + = s_("Branches|This branch hasn’t been merged into %{default_branch}.").html_safe % { default_branch: default_branch } + = s_("Branches|To avoid data loss, consider merging this branch before deleting it.") %p Once you confirm and press = succeed ',' do diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml index 883922dbf04..9d85e027ac9 100644 --- a/app/views/projects/buttons/_download.html.haml +++ b/app/views/projects/buttons/_download.html.haml @@ -1,4 +1,4 @@ -- pipeline = local_assigns.fetch(:pipeline) { project.pipelines.latest_successful_for(ref) } +- pipeline = local_assigns.fetch(:pipeline) { project.latest_successful_pipeline_for(ref) } - if !project.empty_repo? && can?(current_user, :download_code, project) .project-action-button.dropdown.inline> @@ -26,18 +26,16 @@ %i.fa.fa-download %span= _('Download tar') - - if pipeline - - artifacts = pipeline.builds.latest.with_artifacts - - if artifacts.any? - %li.dropdown-header Artifacts - - unless pipeline.latest? - - latest_pipeline = project.pipeline_for(ref) - %li - .unclickable= ci_status_for_statuseable(latest_pipeline) - %li.dropdown-header Previous Artifacts - - artifacts.each do |job| - %li - = link_to latest_succeeded_project_artifacts_path(project, "#{ref}/download", job: job.name), rel: 'nofollow', download: '' do - %i.fa.fa-download - %span - #{ s_('DownloadArtifacts|Download') } '#{job.name}' + - if pipeline && pipeline.latest_builds_with_artifacts.any? + %li.dropdown-header Artifacts + - unless pipeline.latest? + - latest_pipeline = project.pipeline_for(ref) + %li + .unclickable= ci_status_for_statuseable(latest_pipeline) + %li.dropdown-header Previous Artifacts + - pipeline.latest_builds_with_artifacts.each do |job| + %li + = link_to latest_succeeded_project_artifacts_path(project, "#{ref}/download", job: job.name), rel: 'nofollow', download: '' do + %i.fa.fa-download + %span + #{s_('DownloadArtifacts|Download')} '#{job.name}' diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index b8655808d89..a16ffb433a5 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -32,7 +32,7 @@ .commiter - commit_author_link = commit_author_link(commit, avatar: false, size: 24) - - commit_timeago = time_ago_with_tooltip(commit.committed_date) + - commit_timeago = time_ago_with_tooltip(commit.committed_date, placement: 'bottom') - commit_text = _('%{commit_author_link} committed %{commit_timeago}') % { commit_author_link: commit_author_link, commit_timeago: commit_timeago } #{ commit_text.html_safe } diff --git a/app/views/projects/compare/_form.html.haml b/app/views/projects/compare/_form.html.haml index 94b7db5eb25..a518fced2b4 100644 --- a/app/views/projects/compare/_form.html.haml +++ b/app/views/projects/compare/_form.html.haml @@ -2,22 +2,22 @@ .clearfix - if params[:to] && params[:from] .compare-switch-container - = link_to icon('exchange'), {from: params[:to], to: params[:from]}, {class: 'commits-compare-switch has-tooltip btn btn-white', title: 'Switch base of comparison'} - .form-group.dropdown.compare-form-group.from.js-compare-from-dropdown - .input-group.inline-input-group - %span.input-group-addon from - = hidden_field_tag :from, params[:from] - = button_tag type: 'button', title: params[:from], class: "form-control compare-dropdown-toggle js-compare-dropdown has-tooltip git-revision-dropdown-toggle", required: true, data: { refs_url: refs_project_path(@project), toggle: "dropdown", target: ".js-compare-from-dropdown", selected: params[:from], field_name: :from } do - .dropdown-toggle-text.str-truncated= params[:from] || 'Select branch/tag' - = render 'shared/ref_dropdown' - .compare-ellipsis.inline ... + = link_to icon('exchange'), { from: params[:to], to: params[:from] }, class: 'commits-compare-switch has-tooltip btn btn-white', title: 'Swap revisions' .form-group.dropdown.compare-form-group.to.js-compare-to-dropdown .input-group.inline-input-group - %span.input-group-addon to + %span.input-group-addon Source = hidden_field_tag :to, params[:to] = button_tag type: 'button', title: params[:to], class: "form-control compare-dropdown-toggle js-compare-dropdown has-tooltip git-revision-dropdown-toggle", required: true, data: { refs_url: refs_project_path(@project), toggle: "dropdown", target: ".js-compare-to-dropdown", selected: params[:to], field_name: :to } do .dropdown-toggle-text.str-truncated= params[:to] || 'Select branch/tag' = render 'shared/ref_dropdown' + .compare-ellipsis.inline ... + .form-group.dropdown.compare-form-group.from.js-compare-from-dropdown + .input-group.inline-input-group + %span.input-group-addon Target + = hidden_field_tag :from, params[:from] + = button_tag type: 'button', title: params[:from], class: "form-control compare-dropdown-toggle js-compare-dropdown has-tooltip git-revision-dropdown-toggle", required: true, data: { refs_url: refs_project_path(@project), toggle: "dropdown", target: ".js-compare-from-dropdown", selected: params[:from], field_name: :from } do + .dropdown-toggle-text.str-truncated= params[:from] || 'Select branch/tag' + = render 'shared/ref_dropdown' = button_tag "Compare", class: "btn btn-create commits-compare-btn" - if @merge_request.present? diff --git a/app/views/projects/compare/index.html.haml b/app/views/projects/compare/index.html.haml index 2632fea6eba..1ce3ad0c0fd 100644 --- a/app/views/projects/compare/index.html.haml +++ b/app/views/projects/compare/index.html.haml @@ -7,13 +7,19 @@ .sub-header-block Compare Git revisions. %br - Fill input field with commit SHA like - %code.ref-name 4eedf23 - or branch/tag name like - %code.ref-name master - and press compare button for the commits list and a code diff. + Choose a branch/tag (e.g. + = succeed ')' do + %code.ref-name master + or enter a commit SHA (e.g. + = succeed ')' do + %code.ref-name 4eedf23 + to see what's changed or to create a merge request. %br - Changes are shown <b>from</b> the version in the first field <b>to</b> the version in the second field. + Changes are shown as if the + %b source + revision was being merged into the + %b target + revision. .prepend-top-20 = render "form" diff --git a/app/views/projects/diffs/_stats.html.haml b/app/views/projects/diffs/_stats.html.haml index ad2d355ab4a..2de2cf9e38c 100644 --- a/app/views/projects/diffs/_stats.html.haml +++ b/app/views/projects/diffs/_stats.html.haml @@ -21,9 +21,9 @@ %ul - diff_files.each do |diff_file| %li - %a{ href: "##{hexdigest(diff_file.file_path)}", title: diff_file.new_path } + %a.diff-changed-file{ href: "##{hexdigest(diff_file.file_path)}", title: diff_file.new_path } = icon("#{diff_file_changed_icon(diff_file)} fw", class: "#{diff_file_changed_icon_color(diff_file)} append-right-5") - %span.diff-file-changes-path= diff_file.new_path + %span.diff-file-changes-path.append-right-5= diff_file.new_path .pull-right %span.cgreen< +#{diff_file.added_lines} diff --git a/app/views/projects/diffs/viewers/_image.html.haml b/app/views/projects/diffs/viewers/_image.html.haml index aa004a739d7..01879556894 100644 --- a/app/views/projects/diffs/viewers/_image.html.haml +++ b/app/views/projects/diffs/viewers/_image.html.haml @@ -41,10 +41,10 @@ .swipe.view.hide .swipe-frame .frame.deleted - = image_tag(old_blob_raw_path, alt: diff_file.old_path) + = image_tag(old_blob_raw_path, alt: diff_file.old_path, lazy: false) .swipe-wrap .frame.added - = image_tag(blob_raw_path, alt: diff_file.new_path) + = image_tag(blob_raw_path, alt: diff_file.new_path, lazy: false) %span.swipe-bar %span.top-handle %span.bottom-handle @@ -52,9 +52,9 @@ .onion-skin.view.hide .onion-skin-frame .frame.deleted - = image_tag(old_blob_raw_path, alt: diff_file.old_path) + = image_tag(old_blob_raw_path, alt: diff_file.old_path, lazy: false) .frame.added - = image_tag(blob_raw_path, alt: diff_file.new_path) + = image_tag(blob_raw_path, alt: diff_file.new_path, lazy: false) .controls .transparent .drag-track diff --git a/app/views/projects/runners/_form.html.haml b/app/views/projects/runners/_form.html.haml index ac8e15a48b2..e660fce652f 100644 --- a/app/views/projects/runners/_form.html.haml +++ b/app/views/projects/runners/_form.html.haml @@ -11,7 +11,7 @@ .col-sm-10 .checkbox = f.check_box :access_level, {}, 'ref_protected', 'not_protected' - %span.light This runner will only run on pipelines trigged on protected branches + %span.light This runner will only run on pipelines triggered on protected branches .form-group = label :run_untagged, 'Run untagged jobs', class: 'control-label' .col-sm-10 @@ -39,6 +39,6 @@ Tags .col-sm-10 = f.text_field :tag_list, value: runner.tag_list.sort.join(', '), class: 'form-control' - .help-block You can setup jobs to only use Runners with specific tags + .help-block You can setup jobs to only use Runners with specific tags. Separate tags with commas. .form-actions = f.submit 'Save changes', class: 'btn btn-save' diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index c4ed7f6e750..d3f0aa2d339 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -11,13 +11,13 @@ - if params[:author_id].present? = hidden_field_tag(:author_id, params[:author_id]) = dropdown_tag(user_dropdown_label(params[:author_id], "Author"), options: { toggle_class: "js-user-search js-filter-submit js-author-search", title: "Filter by author", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-author js-filter-submit", - placeholder: "Search authors", data: { any_user: "Any Author", first_user: current_user.try(:username), current_user: true, project_id: @project.try(:id), selected: params[:author_id], field_name: "author_id", default_label: "Author" } }) + placeholder: "Search authors", data: { any_user: "Any Author", first_user: current_user&.username, current_user: true, project_id: @project&.id, group_id: @group&.id, selected: params[:author_id], field_name: "author_id", default_label: "Author" } }) .filter-item.inline - if params[:assignee_id].present? = hidden_field_tag(:assignee_id, params[:assignee_id]) = dropdown_tag(user_dropdown_label(params[:assignee_id], "Assignee"), options: { toggle_class: "js-user-search js-filter-submit js-assignee-search", title: "Filter by assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit", - placeholder: "Search assignee", data: { any_user: "Any Assignee", first_user: current_user.try(:username), null_user: true, current_user: true, project_id: @project.try(:id), group_id: @group&.id, selected: params[:assignee_id], field_name: "assignee_id", default_label: "Assignee" } }) + placeholder: "Search assignee", data: { any_user: "Any Assignee", first_user: current_user&.username, null_user: true, current_user: true, project_id: @project&.id, group_id: @group&.id, selected: params[:assignee_id], field_name: "assignee_id", default_label: "Assignee" } }) .filter-item.inline.milestone-filter = render "shared/issuable/milestone_dropdown", selected: finder.milestones.try(:first), name: :milestone_title, show_any: true, show_upcoming: true, show_started: true diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 0afa48b392c..9cae3f51825 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -24,9 +24,9 @@ .block.milestone .sidebar-collapsed-icon = icon('clock-o', 'aria-hidden': 'true') - %span + %span.milestone-title - if issuable.milestone - %span.has-tooltip{ title: milestone_remaining_days(issuable.milestone), data: { container: 'body', html: 1, placement: 'left' } } + %span.has-tooltip{ title: "#{issuable.milestone.title}<br>#{milestone_remaining_days(issuable.milestone)}", data: { container: 'body', html: 1, placement: 'left' } } = issuable.milestone.title - else None diff --git a/changelogs/unreleased/13711-allow-same-period-housekeeping.yml b/changelogs/unreleased/13711-allow-same-period-housekeeping.yml new file mode 100644 index 00000000000..6749e22cf6a --- /dev/null +++ b/changelogs/unreleased/13711-allow-same-period-housekeeping.yml @@ -0,0 +1,6 @@ +--- +title: Allow to use same periods for different housekeeping tasks (effectively + skipping the lesser task) +merge_request: 13711 +author: @cernvcs +type: added diff --git a/changelogs/unreleased/18308-escape-characters.yml b/changelogs/unreleased/18308-escape-characters.yml new file mode 100644 index 00000000000..8766e971490 --- /dev/null +++ b/changelogs/unreleased/18308-escape-characters.yml @@ -0,0 +1,5 @@ +--- +title: Escape quotes in git username +merge_request: 14020 +author: Brandon Everett +type: fixed diff --git a/changelogs/unreleased/20824-scope-users-to-members-in-group-issuable-list.yml b/changelogs/unreleased/20824-scope-users-to-members-in-group-issuable-list.yml new file mode 100644 index 00000000000..245b8129de8 --- /dev/null +++ b/changelogs/unreleased/20824-scope-users-to-members-in-group-issuable-list.yml @@ -0,0 +1,5 @@ +--- +title: Return only group's members in user dropdowns on issuables list pages +merge_request: 14249 +author: +type: changed diff --git a/changelogs/unreleased/21331-improve-confusing-compare-page.yml b/changelogs/unreleased/21331-improve-confusing-compare-page.yml new file mode 100644 index 00000000000..469cc04930b --- /dev/null +++ b/changelogs/unreleased/21331-improve-confusing-compare-page.yml @@ -0,0 +1,5 @@ +--- +title: Make the labels in the Compare form less confusing +merge_request: 14225 +author: +type: changed diff --git a/changelogs/unreleased/31358_decrease_perceived_complexity_threshold_step2.yml b/changelogs/unreleased/31358_decrease_perceived_complexity_threshold_step2.yml new file mode 100644 index 00000000000..6036e1a43a0 --- /dev/null +++ b/changelogs/unreleased/31358_decrease_perceived_complexity_threshold_step2.yml @@ -0,0 +1,5 @@ +--- +title: Decrease Perceived Complexity threshold to 15 +merge_request: 14160 +author: Maxim Rydkin +type: other diff --git a/changelogs/unreleased/31362_decrease_cyclomatic_complexity_threshold_step4.yml b/changelogs/unreleased/31362_decrease_cyclomatic_complexity_threshold_step4.yml new file mode 100644 index 00000000000..a404456198a --- /dev/null +++ b/changelogs/unreleased/31362_decrease_cyclomatic_complexity_threshold_step4.yml @@ -0,0 +1,5 @@ +--- +title: Decrease Cyclomatic Complexity threshold to 13 +merge_request: 14152 +author: Maxim Rydkin +type: other diff --git a/changelogs/unreleased/34510-board-issues-sql-speedup.yml b/changelogs/unreleased/34510-board-issues-sql-speedup.yml new file mode 100644 index 00000000000..244ff7e9dfa --- /dev/null +++ b/changelogs/unreleased/34510-board-issues-sql-speedup.yml @@ -0,0 +1,5 @@ +--- +title: Optimize the boards' issues fetching. +merge_request: 14198 +author: +type: other diff --git a/changelogs/unreleased/35012-navigation-add-option-to-change-navigation-color-palette.yml b/changelogs/unreleased/35012-navigation-add-option-to-change-navigation-color-palette.yml new file mode 100644 index 00000000000..74aa337a18c --- /dev/null +++ b/changelogs/unreleased/35012-navigation-add-option-to-change-navigation-color-palette.yml @@ -0,0 +1,5 @@ +--- +title: Add option in preferences to change navigation theme color +merge_request: +author: +type: added diff --git a/changelogs/unreleased/35917_create_services_for_keys.yml b/changelogs/unreleased/35917_create_services_for_keys.yml new file mode 100644 index 00000000000..e7cad5a11d5 --- /dev/null +++ b/changelogs/unreleased/35917_create_services_for_keys.yml @@ -0,0 +1,4 @@ +--- +title: creation of keys moved to services +merge_request: 13331 +author: haseebeqx diff --git a/changelogs/unreleased/35978-milestone-title.yml b/changelogs/unreleased/35978-milestone-title.yml new file mode 100644 index 00000000000..1a4b71328c1 --- /dev/null +++ b/changelogs/unreleased/35978-milestone-title.yml @@ -0,0 +1,5 @@ +--- +title: Truncate milestone title if sidebar is collapsed +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/36953-add-gitLab-pages-version-to-admin-dashboard.yml b/changelogs/unreleased/36953-add-gitLab-pages-version-to-admin-dashboard.yml new file mode 100644 index 00000000000..680ef0cef92 --- /dev/null +++ b/changelogs/unreleased/36953-add-gitLab-pages-version-to-admin-dashboard.yml @@ -0,0 +1,5 @@ +--- +title: Add GitLab-Pages version to Admin Dashboard +merge_request: 14040 +author: @travismiller +type: added diff --git a/changelogs/unreleased/37025-error-500-in-non-utf8-branch-names.yml b/changelogs/unreleased/37025-error-500-in-non-utf8-branch-names.yml new file mode 100644 index 00000000000..f3118cf0f2f --- /dev/null +++ b/changelogs/unreleased/37025-error-500-in-non-utf8-branch-names.yml @@ -0,0 +1,4 @@ +--- +title: Fixed non-UTF-8 valid branch names from causing an error. +merge_request: 14090 +type: fixed diff --git a/changelogs/unreleased/37405-admin-page-runner-tag-help-update.yml b/changelogs/unreleased/37405-admin-page-runner-tag-help-update.yml new file mode 100644 index 00000000000..bec7da26b1a --- /dev/null +++ b/changelogs/unreleased/37405-admin-page-runner-tag-help-update.yml @@ -0,0 +1,5 @@ +--- +title: 'Add help text to runner edit: tags should be separated by commas.' +merge_request: +author: Brendan O'Leary +type: added diff --git a/changelogs/unreleased/37576-renamed-files-have-escaped-html-for-the-inline-diff-in-the-header.yml b/changelogs/unreleased/37576-renamed-files-have-escaped-html-for-the-inline-diff-in-the-header.yml new file mode 100644 index 00000000000..8c328eb0950 --- /dev/null +++ b/changelogs/unreleased/37576-renamed-files-have-escaped-html-for-the-inline-diff-in-the-header.yml @@ -0,0 +1,5 @@ +--- +title: Fix the diff file header from being html escaped for renamed files. +merge_request: 14121 +author: +type: fixed diff --git a/changelogs/unreleased/37629-lazy-image-loading-breaks-notification-mails-for-an-added-screenshot.yml b/changelogs/unreleased/37629-lazy-image-loading-breaks-notification-mails-for-an-added-screenshot.yml new file mode 100644 index 00000000000..5735d59a2bd --- /dev/null +++ b/changelogs/unreleased/37629-lazy-image-loading-breaks-notification-mails-for-an-added-screenshot.yml @@ -0,0 +1,5 @@ +--- +title: Image attachments are properly displayed in notification emails again +merge_request: 14161 +author: +type: fixed diff --git a/changelogs/unreleased/37730-image-onion-skin-does-not-work-anymore.yml b/changelogs/unreleased/37730-image-onion-skin-does-not-work-anymore.yml new file mode 100644 index 00000000000..7357c61b5f2 --- /dev/null +++ b/changelogs/unreleased/37730-image-onion-skin-does-not-work-anymore.yml @@ -0,0 +1,5 @@ +--- +title: Resolve Image onion skin + swipe does not work anymore +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/37759-also-treat-newlines-as-separator.yml b/changelogs/unreleased/37759-also-treat-newlines-as-separator.yml new file mode 100644 index 00000000000..6894e650c11 --- /dev/null +++ b/changelogs/unreleased/37759-also-treat-newlines-as-separator.yml @@ -0,0 +1,5 @@ +--- +title: Allow using newlines in pipeline email service recipients +merge_request: 14250 +author: +type: fixed diff --git a/changelogs/unreleased/5836-move-lib-ci-into-gitlab-namespace.yml b/changelogs/unreleased/5836-move-lib-ci-into-gitlab-namespace.yml new file mode 100644 index 00000000000..44e16512bae --- /dev/null +++ b/changelogs/unreleased/5836-move-lib-ci-into-gitlab-namespace.yml @@ -0,0 +1,5 @@ +--- +title: Move `lib/ci` to `lib/gitlab/ci` +merge_request: 14078 +author: Maxim Rydkin +type: other diff --git a/changelogs/unreleased/change-dashed-border-button-color.yml b/changelogs/unreleased/change-dashed-border-button-color.yml new file mode 100644 index 00000000000..038bea79273 --- /dev/null +++ b/changelogs/unreleased/change-dashed-border-button-color.yml @@ -0,0 +1,5 @@ +--- +title: changed dashed border button color to be darker +merge_request: !14041 +author: +type: other diff --git a/changelogs/unreleased/changes-tab-jumping.yml b/changelogs/unreleased/changes-tab-jumping.yml new file mode 100644 index 00000000000..5740dfade9f --- /dev/null +++ b/changelogs/unreleased/changes-tab-jumping.yml @@ -0,0 +1,5 @@ +--- +title: Fixed merge request changes bar jumping +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/ci-environment-status-performance.yml b/changelogs/unreleased/ci-environment-status-performance.yml new file mode 100644 index 00000000000..8812733b5a7 --- /dev/null +++ b/changelogs/unreleased/ci-environment-status-performance.yml @@ -0,0 +1,5 @@ +--- +title: Constrain environment deployments to project IDs +merge_request: +author: +type: other diff --git a/changelogs/unreleased/consistent-tooltip-direction-on-commits.yml b/changelogs/unreleased/consistent-tooltip-direction-on-commits.yml new file mode 100644 index 00000000000..9e6a429f6f0 --- /dev/null +++ b/changelogs/unreleased/consistent-tooltip-direction-on-commits.yml @@ -0,0 +1,5 @@ +--- +title: Tooltips in the commit info box now all face the same direction +merge_request: +author: Jedidiah Broadbent +type: fixed diff --git a/changelogs/unreleased/conv-dev-index-regression.yml b/changelogs/unreleased/conv-dev-index-regression.yml new file mode 100644 index 00000000000..799eafa4265 --- /dev/null +++ b/changelogs/unreleased/conv-dev-index-regression.yml @@ -0,0 +1,5 @@ +--- +title: Fix ConvDev Index nav item and Monitoring submenu regression +merge_request: !14124 +author: +type: fixed diff --git a/changelogs/unreleased/dashboards-projects-controller.yml b/changelogs/unreleased/dashboards-projects-controller.yml new file mode 100644 index 00000000000..8b350f70a80 --- /dev/null +++ b/changelogs/unreleased/dashboards-projects-controller.yml @@ -0,0 +1,5 @@ +--- +title: Eager load namespace owners for project dashboards +merge_request: +author: +type: other diff --git a/changelogs/unreleased/detect-orphaned-repositories.yml b/changelogs/unreleased/detect-orphaned-repositories.yml new file mode 100644 index 00000000000..101c1897826 --- /dev/null +++ b/changelogs/unreleased/detect-orphaned-repositories.yml @@ -0,0 +1,5 @@ +--- +title: Scripts to detect orphaned repositories +merge_request: 14204 +author: +type: added diff --git a/changelogs/unreleased/disallow-null-values-for-environments-project-id.yml b/changelogs/unreleased/disallow-null-values-for-environments-project-id.yml new file mode 100644 index 00000000000..f4a956e6724 --- /dev/null +++ b/changelogs/unreleased/disallow-null-values-for-environments-project-id.yml @@ -0,0 +1,5 @@ +--- +title: "Disallow NULL values for environments.project_id" +merge_request: +author: +type: other diff --git a/changelogs/unreleased/fix-gpg-tmp-dir-removal-race-condition.yml b/changelogs/unreleased/fix-gpg-tmp-dir-removal-race-condition.yml new file mode 100644 index 00000000000..e75f188913f --- /dev/null +++ b/changelogs/unreleased/fix-gpg-tmp-dir-removal-race-condition.yml @@ -0,0 +1,5 @@ +--- +title: Fixes the 500 errors caused by a race condition in GPG's tmp directory handling +merge_request: 14194 +author: Alexis Reigel +type: fixed diff --git a/changelogs/unreleased/fix-sidebar-with-scrollbars.yml b/changelogs/unreleased/fix-sidebar-with-scrollbars.yml new file mode 100644 index 00000000000..e0b3851b97f --- /dev/null +++ b/changelogs/unreleased/fix-sidebar-with-scrollbars.yml @@ -0,0 +1,5 @@ +--- +title: Fixed the sidebar scrollbar overlapping links +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/fix-sm-37559-pipeline-triggered-through-api-not-showing-trigger-variables.yml b/changelogs/unreleased/fix-sm-37559-pipeline-triggered-through-api-not-showing-trigger-variables.yml new file mode 100644 index 00000000000..8aae0f6f5b6 --- /dev/null +++ b/changelogs/unreleased/fix-sm-37559-pipeline-triggered-through-api-not-showing-trigger-variables.yml @@ -0,0 +1,6 @@ +--- +title: Fix Pipeline Triggers to show triggered label and predefined variables (e.g. + CI_PIPELINE_TRIGGERED) +merge_request: 14244 +author: +type: fixed diff --git a/changelogs/unreleased/hide-read-registry-scope-when-registry-disabled.yml b/changelogs/unreleased/hide-read-registry-scope-when-registry-disabled.yml new file mode 100644 index 00000000000..22ac9b9073f --- /dev/null +++ b/changelogs/unreleased/hide-read-registry-scope-when-registry-disabled.yml @@ -0,0 +1,4 @@ +--- +title: Hide read_registry scope when registry is disabled on instance +merge_request: 13314 +author: Robin Bobbitt diff --git a/changelogs/unreleased/ie-event-polyfill.yml b/changelogs/unreleased/ie-event-polyfill.yml new file mode 100644 index 00000000000..eaab089a47e --- /dev/null +++ b/changelogs/unreleased/ie-event-polyfill.yml @@ -0,0 +1,5 @@ +--- +title: Adds Event polyfill for IE11 +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/import-sources-fix.yml b/changelogs/unreleased/import-sources-fix.yml new file mode 100644 index 00000000000..03e23bc617c --- /dev/null +++ b/changelogs/unreleased/import-sources-fix.yml @@ -0,0 +1,5 @@ +--- +title: Read import sources from setting at first initialization +merge_request: 14141 +author: Visay Keo +type: fixed diff --git a/changelogs/unreleased/issue_37640.yml b/changelogs/unreleased/issue_37640.yml new file mode 100644 index 00000000000..d806ed64bed --- /dev/null +++ b/changelogs/unreleased/issue_37640.yml @@ -0,0 +1,6 @@ +--- +title: Fix project feature being deleted when updating project with invalid visibility + level +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/italicized_emoji.yml b/changelogs/unreleased/italicized_emoji.yml new file mode 100644 index 00000000000..d3f15f94363 --- /dev/null +++ b/changelogs/unreleased/italicized_emoji.yml @@ -0,0 +1,5 @@ +--- +title: Update native unicode emojis to always render as normal text (previously could render italicized) +merge_request: +author: Branka Martinovic +type: fixed diff --git a/changelogs/unreleased/memoize-the-latest-builds-of-a-pipeline.yml b/changelogs/unreleased/memoize-the-latest-builds-of-a-pipeline.yml new file mode 100644 index 00000000000..5a7cd42b888 --- /dev/null +++ b/changelogs/unreleased/memoize-the-latest-builds-of-a-pipeline.yml @@ -0,0 +1,5 @@ +--- +title: "Memoize the latest builds of a pipeline on a project's homepage" +merge_request: +author: +type: other diff --git a/changelogs/unreleased/projects-controller-show.yml b/changelogs/unreleased/projects-controller-show.yml new file mode 100644 index 00000000000..25f4a72710b --- /dev/null +++ b/changelogs/unreleased/projects-controller-show.yml @@ -0,0 +1,5 @@ +--- +title: Memoize pipelines for project download buttons +merge_request: +author: +type: other diff --git a/changelogs/unreleased/refactor-animate-js.yml b/changelogs/unreleased/refactor-animate-js.yml new file mode 100644 index 00000000000..ec32d68bbdd --- /dev/null +++ b/changelogs/unreleased/refactor-animate-js.yml @@ -0,0 +1,5 @@ +--- +title: Remove animate.js and label animation. +merge_request: +author: +type: removed diff --git a/changelogs/unreleased/refactor-monitoring-service.yml b/changelogs/unreleased/refactor-monitoring-service.yml new file mode 100644 index 00000000000..685397cadb8 --- /dev/null +++ b/changelogs/unreleased/refactor-monitoring-service.yml @@ -0,0 +1,5 @@ +--- +title: Perform prometheus data endpoint requests in parallel +merge_request: 14003 +author: +type: fixed diff --git a/changelogs/unreleased/replace_emails-feature.yml b/changelogs/unreleased/replace_emails-feature.yml new file mode 100644 index 00000000000..d7f1a7a7ba9 --- /dev/null +++ b/changelogs/unreleased/replace_emails-feature.yml @@ -0,0 +1,5 @@ +--- +title: Replace the profile/emails.feature spinach test with an rspec analog +merge_request: 14172 +author: Vitaliy @blackst0ne Klachkov +type: other diff --git a/changelogs/unreleased/replace_group_links-feature.yml b/changelogs/unreleased/replace_group_links-feature.yml new file mode 100644 index 00000000000..7dd157632c9 --- /dev/null +++ b/changelogs/unreleased/replace_group_links-feature.yml @@ -0,0 +1,5 @@ +--- +title: Replace project/group_links.feature spinach test with an rspec analog +merge_request: 14169 +author: Vitaliy @blackst0ne Klachkov +type: other diff --git a/changelogs/unreleased/replace_milestone-feature.yml b/changelogs/unreleased/replace_milestone-feature.yml new file mode 100644 index 00000000000..effe6d65645 --- /dev/null +++ b/changelogs/unreleased/replace_milestone-feature.yml @@ -0,0 +1,5 @@ +--- +title: Replace the project/milestone.feature spinach test with an rspec analog +merge_request: 14171 +author: Vitaliy @blackst0ne Klachkov +type: other diff --git a/changelogs/unreleased/replace_profile_active_tab-feature.yml b/changelogs/unreleased/replace_profile_active_tab-feature.yml new file mode 100644 index 00000000000..e911396a2b9 --- /dev/null +++ b/changelogs/unreleased/replace_profile_active_tab-feature.yml @@ -0,0 +1,5 @@ +--- +title: Replace the 'profile/active_tab.feature' spinach test with an rspec analog +merge_request: 14239 +author: Vitaliy @blackst0ne Klachkov +type: other diff --git a/changelogs/unreleased/replace_project_builds_summary-feature.yml b/changelogs/unreleased/replace_project_builds_summary-feature.yml new file mode 100644 index 00000000000..48652b39b7e --- /dev/null +++ b/changelogs/unreleased/replace_project_builds_summary-feature.yml @@ -0,0 +1,5 @@ +--- +title: Replace the 'project/builds/summary.feature' spinach test with an rspec analog +merge_request: 14177 +author: Vitaliy @blackst0ne Klachkov +type: other diff --git a/changelogs/unreleased/replace_project_issues_award_emoji-feature.yml b/changelogs/unreleased/replace_project_issues_award_emoji-feature.yml new file mode 100644 index 00000000000..a4a7435d4fa --- /dev/null +++ b/changelogs/unreleased/replace_project_issues_award_emoji-feature.yml @@ -0,0 +1,5 @@ +--- +title: Replace the 'project/issues/award_emoji.feature' spinach test with an rspec analog +merge_request: 14202 +author: Vitaliy @blackst0ne Klachkov +type: other diff --git a/changelogs/unreleased/replace_project_merge_requests_accept-feature.yml b/changelogs/unreleased/replace_project_merge_requests_accept-feature.yml new file mode 100644 index 00000000000..03562d6025e --- /dev/null +++ b/changelogs/unreleased/replace_project_merge_requests_accept-feature.yml @@ -0,0 +1,5 @@ +--- +title: Replace the 'project/merge_requests/accept.feature' spinach test with an rspec analog +merge_request: 14176 +author: Vitaliy @blackst0ne Klachkov +type: other diff --git a/changelogs/unreleased/replace_project_merge_requests_revert-feature.yml b/changelogs/unreleased/replace_project_merge_requests_revert-feature.yml new file mode 100644 index 00000000000..7d1ab4566b6 --- /dev/null +++ b/changelogs/unreleased/replace_project_merge_requests_revert-feature.yml @@ -0,0 +1,6 @@ +--- +title: Replace the 'project/merge_requests/revert.feature' spinach test with an rspec + analog +merge_request: 14201 +author: Vitaliy @blackst0ne Klachkov +type: other diff --git a/changelogs/unreleased/replace_spinach_wiki-feature.yml b/changelogs/unreleased/replace_spinach_wiki-feature.yml new file mode 100644 index 00000000000..a1801f1b58d --- /dev/null +++ b/changelogs/unreleased/replace_spinach_wiki-feature.yml @@ -0,0 +1,5 @@ +--- +title: Replace 'project/wiki.feature' spinach test with an rspec analog +merge_request: 13856 +author: Vitaliy @blackst0ne Klachkov +type: other diff --git a/changelogs/unreleased/replace_team_management-feature.yml b/changelogs/unreleased/replace_team_management-feature.yml new file mode 100644 index 00000000000..bc2bb17faf1 --- /dev/null +++ b/changelogs/unreleased/replace_team_management-feature.yml @@ -0,0 +1,5 @@ +--- +title: Replace the project/team_management.feature spinach test with an rspec analog +merge_request: 14173 +author: Vitaliy @blackst0ne Klachkov +type: other diff --git a/changelogs/unreleased/uipolish-fix-remember-me-checkbox.yml b/changelogs/unreleased/uipolish-fix-remember-me-checkbox.yml new file mode 100644 index 00000000000..34aa3d0db6f --- /dev/null +++ b/changelogs/unreleased/uipolish-fix-remember-me-checkbox.yml @@ -0,0 +1,5 @@ +--- +title: Made the "remember me" check boxes have consistent styles and alignment +merge_request: +author: Jedidiah Broadbent +type: fixed diff --git a/changelogs/unreleased/user-recent-push.yml b/changelogs/unreleased/user-recent-push.yml new file mode 100644 index 00000000000..defd5cdfd8e --- /dev/null +++ b/changelogs/unreleased/user-recent-push.yml @@ -0,0 +1,5 @@ +--- +title: Rework how recent push events are retrieved +merge_request: +author: +type: other diff --git a/changelogs/unreleased/winh-protected-branch-modal-merged.yml b/changelogs/unreleased/winh-protected-branch-modal-merged.yml new file mode 100644 index 00000000000..63f1f424a5d --- /dev/null +++ b/changelogs/unreleased/winh-protected-branch-modal-merged.yml @@ -0,0 +1,5 @@ +--- +title: Display whether branch has been merged when deleting protected branch +merge_request: 14220 +author: +type: changed diff --git a/changelogs/unreleased/zj-auto-devops-banner.yml b/changelogs/unreleased/zj-auto-devops-banner.yml new file mode 100644 index 00000000000..a2abed0b2ec --- /dev/null +++ b/changelogs/unreleased/zj-auto-devops-banner.yml @@ -0,0 +1,6 @@ +--- +title: Do not show the Auto DevOps banner when the project has a .gitlab-ci.yml on + master +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/zj-feature-flipper-disable-banner.yml b/changelogs/unreleased/zj-feature-flipper-disable-banner.yml new file mode 100644 index 00000000000..fd5dd1bbe37 --- /dev/null +++ b/changelogs/unreleased/zj-feature-flipper-disable-banner.yml @@ -0,0 +1,5 @@ +--- +title: Allow all AutoDevOps banners to be turned off +merge_request: +author: +type: changed diff --git a/changelogs/unreleased/zj-usage-data-auto-devops.yml b/changelogs/unreleased/zj-usage-data-auto-devops.yml new file mode 100644 index 00000000000..9b5ec894042 --- /dev/null +++ b/changelogs/unreleased/zj-usage-data-auto-devops.yml @@ -0,0 +1,5 @@ +--- +title: Add usage data for Auto DevOps +merge_request: +author: +type: other diff --git a/config/application.rb b/config/application.rb index 32a290f2002..da9bb25c8b9 100644 --- a/config/application.rb +++ b/config/application.rb @@ -51,7 +51,7 @@ module Gitlab # Configure sensitive parameters which will be filtered from the log file. # # Parameters filtered: - # - Any parameter ending with `_token` + # - Any parameter ending with `token` # - Any parameter containing `password` # - Any parameter containing `secret` # - Two-factor tokens (:otp_attempt) @@ -61,7 +61,7 @@ module Gitlab # - Webhook URLs (:hook) # - Sentry DSN (:sentry_dsn) # - Deploy keys (:key) - config.filter_parameters += [/_token$/, /password/, /secret/] + config.filter_parameters += [/token$/, /password/, /secret/] config.filter_parameters += %i( certificate encrypted_key diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index e9661090844..cd44f888d3f 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -76,6 +76,13 @@ production: &base # default_can_create_group: false # default: true # username_changing_enabled: false # default: true - User can change her username/namespace + ## Default theme ID + ## 1 - Indigo + ## 2 - Dark + ## 3 - Light + ## 4 - Blue + ## 5 - Green + # default_theme: 1 # default: 1 ## Automatic issue closing # If a commit message matches this regular expression, all issues referenced from the matched text will be closed. diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 7c1ca05a57b..94429ee91a9 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -232,6 +232,7 @@ Settings['gitlab'] ||= Settingslogic.new({}) Settings.gitlab['default_projects_limit'] ||= 100000 Settings.gitlab['default_branch_protection'] ||= 2 Settings.gitlab['default_can_create_group'] = true if Settings.gitlab['default_can_create_group'].nil? +Settings.gitlab['default_theme'] = Gitlab::Themes::APPLICATION_DEFAULT if Settings.gitlab['default_theme'].nil? Settings.gitlab['host'] ||= ENV['GITLAB_HOST'] || 'localhost' Settings.gitlab['ssh_host'] ||= Settings.gitlab.host Settings.gitlab['https'] = false if Settings.gitlab['https'].nil? @@ -269,7 +270,7 @@ Settings.gitlab.default_projects_features['builds'] = true if Settin Settings.gitlab.default_projects_features['container_registry'] = true if Settings.gitlab.default_projects_features['container_registry'].nil? Settings.gitlab.default_projects_features['visibility_level'] = Settings.__send__(:verify_constant, Gitlab::VisibilityLevel, Settings.gitlab.default_projects_features['visibility_level'], Gitlab::VisibilityLevel::PRIVATE) Settings.gitlab['domain_whitelist'] ||= [] -Settings.gitlab['import_sources'] ||= %w[github bitbucket gitlab google_code fogbugz git gitlab_project gitea] +Settings.gitlab['import_sources'] ||= Gitlab::ImportSources.values Settings.gitlab['trusted_proxies'] ||= [] Settings.gitlab['no_todos_messages'] ||= YAML.load_file(Rails.root.join('config', 'no_todos_messages.yml')) Settings.gitlab['usage_ping_enabled'] = true if Settings.gitlab['usage_ping_enabled'].nil? diff --git a/config/prometheus/additional_metrics.yml b/config/prometheus/additional_metrics.yml index 0642a0b2fe9..33b897f46e2 100644 --- a/config/prometheus/additional_metrics.yml +++ b/config/prometheus/additional_metrics.yml @@ -4,12 +4,21 @@ - title: "Throughput" y_label: "Requests / Sec" required_metrics: - - nginx_upstream_requests_total + - nginx_upstream_responses_total weight: 1 queries: - - query_range: 'sum(rate(nginx_upstream_requests_total{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m]))' - label: Total + - query_range: 'sum(rate(nginx_upstream_responses_total{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) by (status_code)' unit: req / sec + label: Status Code + series: + - label: status_code + when: + - value: 2xx + color: green + - value: 4xx + color: orange + - value: 5xx + color: red - title: "Latency" y_label: "Latency (ms)" required_metrics: @@ -37,9 +46,17 @@ - haproxy_frontend_http_requests_total weight: 1 queries: - - query_range: 'sum(rate(haproxy_frontend_http_requests_total{%{environment_filter}}[2m]))' - label: Total + - query_range: 'sum(rate(haproxy_frontend_http_requests_total{%{environment_filter}}[2m])) by (code)' unit: req / sec + series: + - label: code + when: + - value: 2xx + color: green + - value: 4xx + color: yellow + - value: 5xx + color: red - title: "HTTP Error Rate" y_label: "Error Rate (%)" required_metrics: @@ -86,12 +103,21 @@ - title: "Throughput" y_label: "Requests / Sec" required_metrics: - - nginx_requests_total + - nginx_responses_total weight: 1 queries: - - query_range: 'sum(rate(nginx_requests_total{server_zone!="*", server_zone!="_", %{environment_filter}}[2m]))' - label: Total + - query_range: 'sum(rate(nginx_responses_total{server_zone!="*", server_zone!="_", %{environment_filter}}[2m])) by (status_code)' unit: req / sec + label: Status Code + series: + - label: status_code + when: + - value: 2xx + color: green + - value: 4xx + color: orange + - value: 5xx + color: red - title: "Latency" y_label: "Latency (ms)" required_metrics: @@ -128,6 +154,8 @@ - container_cpu_usage_seconds_total weight: 1 queries: - - query_range: 'sum(rate(container_cpu_usage_seconds_total{container_name!="POD",%{environment_filter}}[2m])) / count(container_cpu_usage_seconds_total{container_name!="POD",%{environment_filter}}) * 100' - label: Average + - query_range: 'sum(rate(container_cpu_usage_seconds_total{container_name!="POD",%{environment_filter}}[2m])) by (cpu) * 100' + label: CPU unit: "%" + series: + - label: cpu diff --git a/db/migrate/20170816234252_add_theme_id_to_users.rb b/db/migrate/20170816234252_add_theme_id_to_users.rb new file mode 100644 index 00000000000..5043f9ec591 --- /dev/null +++ b/db/migrate/20170816234252_add_theme_id_to_users.rb @@ -0,0 +1,10 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddThemeIdToUsers < ActiveRecord::Migration + DOWNTIME = false + + def change + add_column :users, :theme_id, :integer, limit: 2 + end +end diff --git a/db/migrate/20170830131015_swap_event_migration_tables.rb b/db/migrate/20170830131015_swap_event_migration_tables.rb index 5128d1b2fe7..a256de4a8af 100644 --- a/db/migrate/20170830131015_swap_event_migration_tables.rb +++ b/db/migrate/20170830131015_swap_event_migration_tables.rb @@ -7,6 +7,10 @@ class SwapEventMigrationTables < ActiveRecord::Migration # Set this constant to true if this migration requires downtime. DOWNTIME = false + class Event < ActiveRecord::Base + self.table_name = 'events' + end + def up rename_tables end @@ -19,5 +23,25 @@ class SwapEventMigrationTables < ActiveRecord::Migration rename_table :events, :events_old rename_table :events_for_migration, :events rename_table :events_old, :events_for_migration + + # Once swapped we need to reset the primary key of the new "events" table to + # make sure that data created starts with the right value. This isn't + # necessary for events_for_migration since we replicate existing primary key + # values to it. + if Gitlab::Database.postgresql? + reset_primary_key_for_postgresql + else + reset_primary_key_for_mysql + end + end + + def reset_primary_key_for_postgresql + reset_pk_sequence!(Event.table_name) + end + + def reset_primary_key_for_mysql + amount = Event.pluck('COALESCE(MAX(id), 1)').first + + execute "ALTER TABLE #{Event.table_name} AUTO_INCREMENT = #{amount}" end end diff --git a/db/migrate/20170913131410_environments_project_id_not_null.rb b/db/migrate/20170913131410_environments_project_id_not_null.rb new file mode 100644 index 00000000000..d5404f8ede9 --- /dev/null +++ b/db/migrate/20170913131410_environments_project_id_not_null.rb @@ -0,0 +1,16 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class EnvironmentsProjectIdNotNull < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + change_column_null :environments, :project_id, false + end + + def down + change_column_null :environments, :project_id, true + end +end diff --git a/db/migrate/20170914135630_add_index_for_recent_push_events.rb b/db/migrate/20170914135630_add_index_for_recent_push_events.rb new file mode 100644 index 00000000000..99f593b0465 --- /dev/null +++ b/db/migrate/20170914135630_add_index_for_recent_push_events.rb @@ -0,0 +1,40 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddIndexForRecentPushEvents < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_concurrent_index_if_not_present( + :merge_requests, + [:source_project_id, :source_branch] + ) + + remove_concurrent_index_if_present(:merge_requests, :source_project_id) + end + + def down + add_concurrent_index_if_not_present(:merge_requests, :source_project_id) + + remove_concurrent_index_if_present( + :merge_requests, + [:source_project_id, :source_branch] + ) + end + + def add_concurrent_index_if_not_present(table, columns) + return if index_exists?(table, columns) + + add_concurrent_index(table, columns) + end + + def remove_concurrent_index_if_present(table, columns) + return unless index_exists?(table, columns) + + remove_concurrent_index(table, columns) + end +end diff --git a/db/post_migrate/20170913180600_fix_projects_without_project_feature.rb b/db/post_migrate/20170913180600_fix_projects_without_project_feature.rb new file mode 100644 index 00000000000..bfa9ad80c7d --- /dev/null +++ b/db/post_migrate/20170913180600_fix_projects_without_project_feature.rb @@ -0,0 +1,33 @@ +class FixProjectsWithoutProjectFeature < ActiveRecord::Migration + DOWNTIME = false + + def up + # Deletes corrupted project features + sql = "DELETE FROM project_features WHERE project_id IS NULL" + execute(sql) + + # Creates missing project features with private visibility + sql = + %Q{ + INSERT INTO project_features(project_id, repository_access_level, issues_access_level, merge_requests_access_level, wiki_access_level, + builds_access_level, snippets_access_level, created_at, updated_at) + SELECT projects.id as project_id, + 10 as repository_access_level, + 10 as issues_access_level, + 10 as merge_requests_access_level, + 10 as wiki_access_level, + 10 as builds_access_level , + 10 as snippets_access_level, + projects.created_at, + projects.updated_at + FROM projects + LEFT OUTER JOIN project_features ON project_features.project_id = projects.id + WHERE (project_features.id IS NULL) + } + + execute(sql) + end + + def down + end +end diff --git a/db/schema.rb b/db/schema.rb index df941afa7d7..2d8c33591f0 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: 20170905112933) do +ActiveRecord::Schema.define(version: 20170914135630) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -520,7 +520,7 @@ ActiveRecord::Schema.define(version: 20170905112933) do add_index "emails", ["user_id"], name: "index_emails_on_user_id", using: :btree create_table "environments", force: :cascade do |t| - t.integer "project_id" + t.integer "project_id", null: false t.string "name", null: false t.datetime "created_at" t.datetime "updated_at" @@ -892,7 +892,7 @@ ActiveRecord::Schema.define(version: 20170905112933) do add_index "merge_requests", ["head_pipeline_id"], name: "index_merge_requests_on_head_pipeline_id", using: :btree add_index "merge_requests", ["milestone_id"], name: "index_merge_requests_on_milestone_id", using: :btree add_index "merge_requests", ["source_branch"], name: "index_merge_requests_on_source_branch", using: :btree - add_index "merge_requests", ["source_project_id"], name: "index_merge_requests_on_source_project_id", using: :btree + add_index "merge_requests", ["source_project_id", "source_branch"], name: "index_merge_requests_on_source_project_id_and_source_branch", using: :btree add_index "merge_requests", ["target_branch"], name: "index_merge_requests_on_target_branch", using: :btree add_index "merge_requests", ["target_project_id", "iid"], name: "index_merge_requests_on_target_project_id_and_iid", unique: true, using: :btree add_index "merge_requests", ["title"], name: "index_merge_requests_on_title", using: :btree @@ -1608,6 +1608,7 @@ ActiveRecord::Schema.define(version: 20170905112933) do t.boolean "notified_of_own_activity" t.string "preferred_language" t.string "rss_token" + t.integer "theme_id", limit: 2 end add_index "users", ["admin"], name: "index_users_on_admin", using: :btree diff --git a/doc/administration/gitaly/index.md b/doc/administration/gitaly/index.md index 5732b6a1ca4..40099dcc967 100644 --- a/doc/administration/gitaly/index.md +++ b/doc/administration/gitaly/index.md @@ -145,8 +145,8 @@ Omnibus installations: ```ruby # /etc/gitlab/gitlab.rb git_data_dirs({ - { 'default' => { 'path' => '/mnt/gitlab/default', 'gitaly_address' => 'tcp://gitlab.internal:9999' } }, - { 'storage1' => { 'path' => '/mnt/gitlab/storage1', 'gitaly_address' => 'tcp://gitlab.internal:9999' } }, + 'default' => { 'path' => '/mnt/gitlab/default', 'gitaly_address' => 'tcp://gitlab.internal:9999' }, + 'storage1' => { 'path' => '/mnt/gitlab/storage1', 'gitaly_address' => 'tcp://gitlab.internal:9999' }, }) gitlab_rails['gitaly_token'] = 'abc123secret' diff --git a/doc/administration/reply_by_email.md b/doc/administration/reply_by_email.md index e99a7ee29cc..1304476e678 100644 --- a/doc/administration/reply_by_email.md +++ b/doc/administration/reply_by_email.md @@ -77,6 +77,33 @@ and use [an application password](https://support.google.com/mail/answer/185833) To set up a basic Postfix mail server with IMAP access on Ubuntu, follow the [Postfix setup documentation](reply_by_email_postfix_setup.md). +### Security Concerns + +**WARNING:** Be careful when choosing the domain used for receiving incoming +email. + +For the sake of example, suppose your top-level company domain is `hooli.com`. +All employees in your company have an email address at that domain via Google +Apps, and your company's private Slack instance requires a valid `@hooli.com` +email address in order to sign up. + +If you also host a public-facing GitLab instance at `hooli.com` and set your +incoming email domain to `hooli.com`, an attacker could abuse the "Create new +issue by email" feature by using a project's unique address as the email when +signing up for Slack, which would send a confirmation email, which would create +a new issue on the project owned by the attacker, allowing them to click the +confirmation link and validate their account on your company's private Slack +instance. + +We recommend receiving incoming email on a subdomain, such as +`incoming.hooli.com`, and ensuring that you do not employ any services that +authenticate solely based on access to an email domain such as `*.hooli.com.` +Alternatively, use a dedicated domain for GitLab email communications such as +`hooli-gitlab.com`. + +See GitLab issue [#30366](https://gitlab.com/gitlab-org/gitlab-ce/issues/30366) +for a real-world example of this exploit. + ### Omnibus package installations 1. Find the `incoming_email` section in `/etc/gitlab/gitlab.rb`, enable the @@ -141,7 +168,7 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow the # The IDLE command timeout. gitlab_rails['incoming_email_idle_timeout'] = 60 ``` - + ```ruby # Configuration for Microsoft Exchange mail server w/ IMAP enabled, assumes mailbox incoming@exchange.example.com gitlab_rails['incoming_email_enabled'] = true @@ -253,7 +280,7 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow the # The IDLE command timeout. idle_timeout: 60 ``` - + ```yaml # Configuration for Microsoft Exchange mail server w/ IMAP enabled, assumes mailbox incoming@exchange.example.com incoming_email: diff --git a/doc/api/keys.md b/doc/api/keys.md index 376ac27df3a..ddcf7830621 100644 --- a/doc/api/keys.md +++ b/doc/api/keys.md @@ -32,6 +32,7 @@ Parameters: "twitter": "", "website_url": "", "email": "john@example.com", + "theme_id": 2, "color_scheme_id": 1, "projects_limit": 10, "current_sign_in_at": null, diff --git a/doc/api/session.md b/doc/api/session.md index f79eac11689..b97e26f34a2 100644 --- a/doc/api/session.md +++ b/doc/api/session.md @@ -39,6 +39,7 @@ Example response: "twitter": "", "website_url": "", "email": "john@example.com", + "theme_id": 1, "color_scheme_id": 1, "projects_limit": 10, "current_sign_in_at": "2015-07-07T07:10:58.392Z", diff --git a/doc/api/users.md b/doc/api/users.md index 9f3e4caf2f4..6d5db16b36a 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -72,6 +72,7 @@ GET /users "organization": "", "last_sign_in_at": "2012-06-01T11:41:01Z", "confirmed_at": "2012-05-23T09:05:22Z", + "theme_id": 1, "last_activity_on": "2012-05-23", "color_scheme_id": 2, "projects_limit": 100, @@ -105,6 +106,7 @@ GET /users "organization": "", "last_sign_in_at": null, "confirmed_at": "2012-05-30T16:53:06.148Z", + "theme_id": 1, "last_activity_on": "2012-05-23", "color_scheme_id": 3, "projects_limit": 100, @@ -215,6 +217,7 @@ Parameters: "organization": "", "last_sign_in_at": "2012-06-01T11:41:01Z", "confirmed_at": "2012-05-23T09:05:22Z", + "theme_id": 1, "last_activity_on": "2012-05-23", "color_scheme_id": 2, "projects_limit": 100, @@ -341,6 +344,7 @@ GET /user "organization": "", "last_sign_in_at": "2012-06-01T11:41:01Z", "confirmed_at": "2012-05-23T09:05:22Z", + "theme_id": 1, "last_activity_on": "2012-05-23", "color_scheme_id": 2, "projects_limit": 100, @@ -387,6 +391,7 @@ GET /user "organization": "", "last_sign_in_at": "2012-06-01T11:41:01Z", "confirmed_at": "2012-05-23T09:05:22Z", + "theme_id": 1, "last_activity_on": "2012-05-23", "color_scheme_id": 2, "projects_limit": 100, diff --git a/doc/ci/environments.md b/doc/ci/environments.md index c1362b7bd5b..acd5682841a 100644 --- a/doc/ci/environments.md +++ b/doc/ci/environments.md @@ -240,55 +240,18 @@ Remember that if your environment's name is `production` (all lowercase), then it will get recorded in [Cycle Analytics](../user/project/cycle_analytics.md). Double the benefit! -## Web terminals - ->**Note:** -Web terminals were added in GitLab 8.15 and are only available to project -masters and owners. - -If you deploy to your environments with the help of a deployment service (e.g., -the [Kubernetes service][kubernetes-service], GitLab can open -a terminal session to your environment! This is a very powerful feature that -allows you to debug issues without leaving the comfort of your web browser. To -enable it, just follow the instructions given in the service documentation. - -Once enabled, your environments will gain a "terminal" button: - -![Terminal button on environment index](img/environments_terminal_button_on_index.png) - -You can also access the terminal button from the page for a specific environment: - -![Terminal button for an environment](img/environments_terminal_button_on_show.png) - -Wherever you find it, clicking the button will take you to a separate page to -establish the terminal session: - -![Terminal page](img/environments_terminal_page.png) - -This works just like any other terminal - you'll be in the container created -by your deployment, so you can run shell commands and get responses in real -time, check the logs, try out configuration or code tweaks, etc. You can open -multiple terminals to the same environment - they each get their own shell -session - and even a multiplexer like `screen` or `tmux`! - ->**Note:** -Container-based deployments often lack basic tools (like an editor), and may -be stopped or restarted at any time. If this happens, you will lose all your -changes! Treat this as a debugging tool, not a comprehensive online IDE. - ---- - -While this is fine for deploying to some stable environments like staging or -production, what happens for branches? So far we haven't defined anything -regarding deployments for branches other than `master`. Dynamic environments -will help us achieve that. - ## Dynamic environments As the name suggests, it is possible to create environments on the fly by just declaring their names dynamically in `.gitlab-ci.yml`. Dynamic environments is the basis of [Review apps](review_apps/index.md). +>**Note:** +The `name` and `url` parameters can use any of the defined CI variables, +including predefined, secure variables and `.gitlab-ci.yml` +[`variables`](yaml/README.md#variables). +You however cannot use variables defined under `script` or on the Runner's side. + GitLab Runner exposes various [environment variables][variables] when a job runs, and as such, you can use them as environment names. Let's add another job in our example which will deploy to all branches except `master`: @@ -434,7 +397,8 @@ Let's briefly see where URL that's defined in the environments is exposed. ## Making use of the environment URL -The environment URL is exposed in a few places within GitLab. +The [environment URL](yaml/README.md#environments-url) is exposed in a few +places within GitLab. | In a merge request widget as a link | In the Environments view as a button | In the Deployments view as a button | | -------------------- | ------------ | ----------- | @@ -598,7 +562,7 @@ exist, you should see something like: >**Notes:** > -- For the monitor dashboard to appear, you need to: +- For the monitoring dashboard to appear, you need to: - Have enabled the [Prometheus integration][prom] - Configured Prometheus to collect at least one [supported metric](../user/project/integrations/prometheus_library/metrics.md) - With GitLab 9.2, all deployments to an environment are shown directly on the @@ -608,8 +572,7 @@ If you have enabled [Prometheus for monitoring system and response metrics](http Once configured, GitLab will attempt to retrieve [supported performance metrics](https://docs.gitlab.com/ee/user/project/integrations/prometheus_library/metrics.html) for any environment which has had a successful deployment. If monitoring data was -successfully retrieved, a Monitoring button will appear on the environment's -detail page. +successfully retrieved, a Monitoring button will appear for each environment. ![Environment Detail with Metrics](img/prometheus_environment_detail_with_metrics.png) @@ -623,6 +586,49 @@ version of the app, all without leaving GitLab. ![Monitoring dashboard](img/environments_monitoring.png) +## Web terminals + +>**Note:** +Web terminals were added in GitLab 8.15 and are only available to project +masters and owners. + +If you deploy to your environments with the help of a deployment service (e.g., +the [Kubernetes service][kubernetes-service], GitLab can open +a terminal session to your environment! This is a very powerful feature that +allows you to debug issues without leaving the comfort of your web browser. To +enable it, just follow the instructions given in the service documentation. + +Once enabled, your environments will gain a "terminal" button: + +![Terminal button on environment index](img/environments_terminal_button_on_index.png) + +You can also access the terminal button from the page for a specific environment: + +![Terminal button for an environment](img/environments_terminal_button_on_show.png) + +Wherever you find it, clicking the button will take you to a separate page to +establish the terminal session: + +![Terminal page](img/environments_terminal_page.png) + +This works just like any other terminal - you'll be in the container created +by your deployment, so you can run shell commands and get responses in real +time, check the logs, try out configuration or code tweaks, etc. You can open +multiple terminals to the same environment - they each get their own shell +session - and even a multiplexer like `screen` or `tmux`! + +>**Note:** +Container-based deployments often lack basic tools (like an editor), and may +be stopped or restarted at any time. If this happens, you will lose all your +changes! Treat this as a debugging tool, not a comprehensive online IDE. + +--- + +While this is fine for deploying to some stable environments like staging or +production, what happens for branches? So far we haven't defined anything +regarding deployments for branches other than `master`. Dynamic environments +will help us achieve that. + ## Checkout deployments locally Since 8.13, a reference in the git repository is saved for each deployment, so diff --git a/doc/ci/img/environments_monitoring.png b/doc/ci/img/environments_monitoring.png Binary files differindex d9c46ea4c95..dcffdd1fdb8 100644 --- a/doc/ci/img/environments_monitoring.png +++ b/doc/ci/img/environments_monitoring.png diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md index 0737803206e..ebcb92b5db1 100644 --- a/doc/ci/variables/README.md +++ b/doc/ci/variables/README.md @@ -158,17 +158,17 @@ script: settings. Follow the discussion in issue [#13784][ce-13784] for masking the secret variables. -GitLab CI allows you to define per-project or per-group **secret variables** -that are set in the build environment. The secret variables are stored out of -the repository (`.gitlab-ci.yml`) and are securely passed to GitLab Runner -making them available in the build environment. It's the recommended method to -use for storing things like passwords, secret keys and credentials. +GitLab CI allows you to define per-project or per-group secret variables +that are set in the pipeline environment. The secret variables are stored out of +the repository (not in `.gitlab-ci.yml`) and are securely passed to GitLab Runner +making them available during a pipeline run. It's the recommended method to +use for storing things like passwords, SSH keys and credentials. Project-level secret variables can be added by going to your project's -**Settings ➔ CI/CD**, then finding the section called **Secret variables**. +**Settings > CI/CD**, then finding the section called **Secret variables**. Likewise, group-level secret variables can be added by going to your group's -**Settings ➔ CI/CD**, then finding the section called **Secret variables**. +**Settings > CI/CD**, then finding the section called **Secret variables**. Any variables of [subgroups] will be inherited recursively. Once you set them, they will be available for all subsequent pipelines. You can also @@ -185,8 +185,8 @@ protected, it would only be securely passed to pipelines running on the protected variables. Protected variables can be added by going to your project's -**Settings ➔ CI/CD**, then finding the section called -**Secret variables**, and check *Protected*. +**Settings > CI/CD**, then finding the section called +**Secret variables**, and check "Protected". Once you set them, they will be available for all subsequent pipelines. diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 78733b9cc4b..f69d71a5c39 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -727,6 +727,9 @@ deployment to the `production` environment. - Before GitLab 8.11, the name of an environment could be defined as a string like `environment: production`. The recommended way now is to define it under the `name` keyword. +- The `name` parameter can use any of the defined CI variables, + including predefined, secure variables and `.gitlab-ci.yml` [`variables`](#variables). + You however cannot use variables defined under `script`. The `environment` name can contain: @@ -762,6 +765,9 @@ deploy to production: - Introduced in GitLab 8.11. - Before GitLab 8.11, the URL could be added only in GitLab's UI. The recommended way now is to define it in `.gitlab-ci.yml`. +- The `url` parameter can use any of the defined CI variables, + including predefined, secure variables and `.gitlab-ci.yml` [`variables`](#variables). + You however cannot use variables defined under `script`. This is an optional value that when set, it exposes buttons in various places in GitLab which when clicked take you to the defined URL. @@ -841,10 +847,9 @@ The `stop_review_app` job is **required** to have the following keywords defined **Notes:** - [Introduced][ce-6323] in GitLab 8.12 and GitLab Runner 1.6. - The `$CI_ENVIRONMENT_SLUG` was [introduced][ce-7983] in GitLab 8.15. - -`environment` can also represent a configuration hash with `name` and `url`. -These parameters can use any of the defined [CI variables](#variables) -(including predefined, secure variables and `.gitlab-ci.yml` variables). +- The `name` and `url` parameters can use any of the defined CI variables, + including predefined, secure variables and `.gitlab-ci.yml` [`variables`](#variables). + You however cannot use variables defined under `script`. For example: diff --git a/doc/development/README.md b/doc/development/README.md index dd150421b65..3096d9f25f0 100644 --- a/doc/development/README.md +++ b/doc/development/README.md @@ -43,6 +43,7 @@ - [Object state models](object_state_models.md) - [Building a package for testing purposes](build_test_package.md) - [Manage feature flags](feature_flags.md) +- [View sent emails or preview mailers](emails.md) ## Databases @@ -60,6 +61,7 @@ - [Ordering Table Columns](ordering_table_columns.md) - [Verifying Database Capabilities](verifying_database_capabilities.md) - [Hash Indexes](hash_indexes.md) +- [Swapping Tables](swapping_tables.md) ## i18n diff --git a/doc/development/emails.md b/doc/development/emails.md new file mode 100644 index 00000000000..18f47f44cb5 --- /dev/null +++ b/doc/development/emails.md @@ -0,0 +1,23 @@ +# Dealing with email in development + +## Sent emails + +To view rendered emails "sent" in your development instance, visit +[`/rails/letter_opener`](http://localhost:3000/rails/letter_opener). + +## Mailer previews + +Rails provides a way to preview our mailer templates in HTML and plaintext using +dummy data. + +The previews live in [`spec/mailers/previews`][previews] and can be viewed at +[`/rails/mailers`](http://localhost:3000/rails/mailers). + +See the [Rails guides] for more info. + +[previews]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/spec/mailers/previews +[Rails guides]: http://guides.rubyonrails.org/action_mailer_basics.html#previewing-emails + +--- + +[Return to Development documentation](README.md) diff --git a/doc/development/swapping_tables.md b/doc/development/swapping_tables.md new file mode 100644 index 00000000000..6b990ece72c --- /dev/null +++ b/doc/development/swapping_tables.md @@ -0,0 +1,53 @@ +# Swapping Tables + +Sometimes you need to replace one table with another. For example, when +migrating data in a very large table it's often better to create a copy of the +table and insert & migrate the data into this new table in the background. + +Let's say you want to swap the table "events" with "events_for_migration". In +this case you need to follow 3 steps: + +1. Rename "events" to "events_temporary" +2. Rename "events_for_migration" to "events" +3. Rename "events_temporary" to "events_for_migration" + +Rails allows you to do this using the `rename_table` method: + +```ruby +rename_table :events, :events_temporary +rename_table :events_for_migration, :events +rename_table :events_temporary, :events_for_migration +``` + +This does not require any downtime as long as the 3 `rename_table` calls are +executed in the _same_ database transaction. Rails by default uses database +transactions for migrations, but if it doesn't you'll need to start one +manually: + +```ruby +Event.transaction do + rename_table :events, :events_temporary + rename_table :events_for_migration, :events + rename_table :events_temporary, :events_for_migration +end +``` + +Once swapped you _have to_ reset the primary key of the new table. For +PostgreSQL you can use the `reset_pk_sequence!` method like so: + +```ruby +reset_pk_sequence!('events') +``` + +For MySQL however you need to do run the following: + +```ruby +amount = Event.pluck('COALESCE(MAX(id), 1)').first + +execute "ALTER TABLE events AUTO_INCREMENT = #{amount}" +``` + +Failure to reset the primary keys will result in newly created rows starting +with an ID value of 1. Depending on the existing data this can then lead to +duplicate key constraints from popping up, preventing users from creating new +data. diff --git a/doc/install/installation.md b/doc/install/installation.md index 66eb7675896..200cd94f43c 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -299,9 +299,9 @@ sudo usermod -aG redis git ### Clone the Source # Clone GitLab repository - sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 9-5-stable gitlab + sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 10-0-stable gitlab -**Note:** You can change `9-5-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! +**Note:** You can change `10-0-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! ### Configure It diff --git a/doc/install/kubernetes/index.md b/doc/install/kubernetes/index.md index c799f88ad74..467d5b92e0c 100644 --- a/doc/install/kubernetes/index.md +++ b/doc/install/kubernetes/index.md @@ -19,7 +19,7 @@ should be deployed, upgraded, and configured. ## GitLab-Omnibus Chart (Recommended) > **Note**: This chart is in beta while [additional features](https://gitlab.com/charts/charts.gitlab.io/issues/68) are being added. -This chart is the best available way to operate GitLab on Kubernetes. It deploys and configures nearly all features of GitLab, including: a [Runner](https://docs.gitlab.com/runner/), [Container Registry](https://docs.gitlab.com/ee/user/project/container_registry.html#gitlab-container-registry), [Mattermost](https://docs.gitlab.com/omnibus/gitlab-mattermost/), [automatic SSL](https://github.com/kubernetes/charts/tree/master/stable/kube-lego), and a [load balancer](https://github.com/kubernetes/ingress/tree/master/controllers/nginx). It is based on our [GitLab Omnibus Docker Images](https://docs.gitlab.com/omnibus/docker/README.html). +This chart is the best available way to operate GitLab on Kubernetes. It deploys and configures nearly all features of GitLab, including: a [Runner](https://docs.gitlab.com/runner/), [Container Registry](../../user/project/container_registry.html#gitlab-container-registry), [Mattermost](https://docs.gitlab.com/omnibus/gitlab-mattermost/), [automatic SSL](https://github.com/kubernetes/charts/tree/master/stable/kube-lego), and a [load balancer](https://github.com/kubernetes/ingress/tree/master/controllers/nginx). It is based on our [GitLab Omnibus Docker Images](https://docs.gitlab.com/omnibus/docker/README.html). Once the [cloud native charts](#upcoming-cloud-native-helm-charts) are ready for production use, this chart will be deprecated. Due to the difficulty in supporting upgrades to the new architecture, migrating will require exporting data out of this instance and importing it into the new deployment. @@ -41,7 +41,7 @@ This is a large project and will be worked on over the span of multiple releases ### GitLab Runner Chart -If you already have a GitLab instance running, inside or outside of Kubernetes, and you'd like to leverage the Runner's [Kubernetes capabilities](https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/blob/master/docs/executors/kubernetes.md), it can be deployed with the GitLab Runner chart. +If you already have a GitLab instance running, inside or outside of Kubernetes, and you'd like to leverage the Runner's [Kubernetes capabilities](https://docs.gitlab.com/runner/executors/kubernetes.html), it can be deployed with the GitLab Runner chart. Learn more about [gitlab-runner chart.](gitlab_runner_chart.md) diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md index babf44d2665..b31b8eaaca0 100644 --- a/doc/topics/autodevops/index.md +++ b/doc/topics/autodevops/index.md @@ -323,6 +323,23 @@ container registry. **Restarting a pod, scaling a service, or other actions whic require on-going access to the registry will fail**. On-going secure access is planned for a subsequent release. +## Disable the banner instance wide + +If an administrater would like to disable the banners on an instance level, this +feature can be disabled either through the console: + +```basb +$ gitlab-rails console +[1] pry(main)> Feature.get(:auto_devops_banner_disabled).disable +=> true +``` + +Or through the HTTP API with the admin access token: + +``` +curl --data "value=true" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/features/auto_devops_banner_disabled +``` + ## Troubleshooting - Auto Build and Auto Test may fail in detecting your language/framework. There diff --git a/doc/topics/autodevops/quick_start_guide.md b/doc/topics/autodevops/quick_start_guide.md index f23c9d794b4..564dd3222ac 100644 --- a/doc/topics/autodevops/quick_start_guide.md +++ b/doc/topics/autodevops/quick_start_guide.md @@ -18,45 +18,66 @@ example for this guide. It contains two files: ## Fork sample project on GitLab.com Let’s start by forking our sample application. Go to [the project -page](https://gitlab.com/gitlab-examples/minimal-ruby-app) and press the `Fork` -button. Soon you should have a project under your namespace with the necessary -files. +page](https://gitlab.com/auto-devops-examples/minimal-ruby-app) and press the +**Fork** button. Soon you should have a project under your namespace with the +necessary files. ## Setup your own cluster on Google Container Engine -If you do not already have a Google Cloud account, create one at https://console.cloud.google.com. +If you do not already have a Google Cloud account, create one at +https://console.cloud.google.com. -Visit the [`Container Engine`](https://console.cloud.google.com/kubernetes/list) tab and create a new cluster. You can change the name and leave the rest of the default settings. Once you have your cluster running, you need to connect to the cluster by following the Google interface. +Visit the [**Container Engine**](https://console.cloud.google.com/kubernetes/list) +tab and create a new cluster. You can change the name and leave the rest of the +default settings. Once you have your cluster running, you need to connect to the +cluster by following the Google interface. ## Connect to Kubernetes cluster You need to have the Google Cloud SDK installed. e.g. -On OSX, install [homebrew](https://brew.sh): +On macOS, install [homebrew](https://brew.sh): 1. Install Brew Caskroom: `brew install caskroom/cask/brew-cask` 2. Install Google Cloud SDK: `brew cask install google-cloud-sdk` -3. Add `kubectl`: `gcloud components install kubectl` +3. Add `kubectl` with: `gcloud components install kubectl` 4. Log in: `gcloud auth login` -Now go back to the Google interface, find your cluster, and follow the instructions under `Connect to the cluster` and open the Kubernetes Dashboard. It will look something like `gcloud container clusters get-credentials ruby-autodeploy \ --zone europe-west2-c --project api-project-XXXXXXX` and then `kubectl proxy`. +Now go back to the Google interface, find your cluster, follow the instructions +under "Connect to the cluster" and open the Kubernetes Dashboard. It will look +something like: + +```sh +gcloud container clusters get-credentials ruby-autodeploy \ --zone europe-west2-c --project api-project-XXXXXXX +``` + +Finally, run `kubectl proxy`. ![connect to cluster](img/guide_connect_cluster.png) ## Copy credentials to GitLab.com project -Once you have the Kubernetes Dashboard interface running, you should visit `Secrets` under the `Config` section. There you should find the settings we need for GitLab integration: ca.crt and token. +Once you have the Kubernetes Dashboard interface running, you should visit +**Secrets** under the "Config" section. There, you should find the settings we +need for GitLab integration: `ca.crt` and token. ![connect to cluster](img/guide_secret.png) -You need to copy-paste the ca.crt and token into your project on GitLab.com in the Kubernetes integration page under project **Settings > Integrations > Project services > Kubernetes**. Don't actually copy the namespace though. Each project should have a unique namespace, and by leaving it blank, GitLab will create one for you. +You need to copy-paste the `ca.crt` and token into your project on GitLab.com in +the Kubernetes integration page under project +**Settings > Integrations > Project services > Kubernetes**. Don't actually copy +the namespace though. Each project should have a unique namespace, and by leaving +it blank, GitLab will create one for you. ![connect to cluster](img/guide_integration.png) -For API URL, you should use the `Endpoint` IP from your cluster page on Google Cloud Platform. +For the API URL, you should use the "Endpoint" IP from your cluster page on +Google Cloud Platform. ## Expose application to the world -In order to be able to visit your application, you need to install an NGINX ingress controller and point your domain name to its external IP address. +In order to be able to visit your application, you need to install an NGINX +ingress controller and point your domain name to its external IP address. Let's +see how that's done. ### Set up Ingress controller @@ -68,28 +89,49 @@ helm init helm install --name ruby-app stable/nginx-ingress ``` -This should create several services including `ruby-app-nginx-ingress-controller`. You can list your services by running `kubectl get svc` to confirm that. +This should create several services including `ruby-app-nginx-ingress-controller`. +You can list your services by running `kubectl get svc` to confirm that. ### Point DNS at Cluster IP -Find out the external IP address of the `ruby-app-nginx-ingress-controller` by running: +Find out the external IP address of the `ruby-app-nginx-ingress-controller` by +running: ```sh kubectl get svc ruby-app-nginx-ingress-controller -o jsonpath='{.status.loadBalancer.ingress[0].ip}' ``` -Use this IP address to configure your DNS. This part heavily depends on your preferences and domain provider. But in case you are not sure, just create an A record with a wildcard host like `*.<your-domain>`. +Use this IP address to configure your DNS. This part heavily depends on your +preferences and domain provider. But in case you are not sure, just create an +A record with a wildcard host like `*.<your-domain>`. -Use `nslookup minimal-ruby-app-staging.<yourdomain>` to confirm that domain is assigned to the cluster IP. +Use `nslookup minimal-ruby-app-staging.<yourdomain>` to confirm that domain is +assigned to the cluster IP. ## Set up Auto DevOps -In your GitLab.com project, go to **Settings > CI/CD** and find the Auto DevOps section. Select "Enable Auto DevOps", add in your base domain, and save. +In your GitLab.com project, go to **Settings > CI/CD** and find the Auto DevOps +section. Select "Enable Auto DevOps", add in your base domain, and save. ![auto devops settings](img/auto_devops_settings.png) -Then trigger your first pipeline run. This will create a new pipeline with several jobs: `build`, `test`, `codequality`, and `production`. The `build` job will create a docker image with your new change and push it to the GitLab Container Registry. The `test` job will test your change. The `codequality` job will run static analysis on your change. The `production` job will deploy your change to a production application. Once the deploy job succeeds you should be able to see your application by visiting the Kubernetes dashboard. Select the namespace of your project, which will look like `minimal-ruby-app-23`, but with a unique ID for your project, and your app will be listed as "production" under the Deployment tab. +Next, a pipeline needs to be triggered. Since the test project doesn't have a +`.gitlab-ci.yml`, you need to either push a change to the repository or +manually visit `https://gitlab.com/<username>/minimal-ruby-app/pipelines/run`, +where `<username>` is your username. + +This will create a new pipeline with several jobs: `build`, `test`, `codequality`, +and `production`. The `build` job will create a Docker image with your new +change and push it to the Container Registry. The `test` job will test your +changes, whereas the `codequality` job will run static analysis on your changes. +Finally, the `production` job will deploy your changes to a production application. + +Once the deploy job succeeds you should be able to see your application by +visiting the Kubernetes dashboard. Select the namespace of your project, which +will look like `minimal-ruby-app-23`, but with a unique ID for your project, +and your app will be listed as "production" under the Deployment tab. -Once its ready - just visit http://minimal-ruby-app.example.com to see “Hello, world!” +Once its ready, just visit `http://minimal-ruby-app.example.com` to see the +famous "Hello, world!"! [ce-37115]: https://gitlab.com/gitlab-org/gitlab-ce/issues/37115 diff --git a/doc/update/8.17-to-9.0.md b/doc/update/8.17-to-9.0.md index 2abc57da1a0..baab217b6b7 100644 --- a/doc/update/8.17-to-9.0.md +++ b/doc/update/8.17-to-9.0.md @@ -236,7 +236,7 @@ ActionMailer::Base.delivery_method = :smtp See [smtp_settings.rb.sample] as an example. -[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-17-stable/config/initializers/smtp_settings.rb.sample#L13 +[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/9-0-stable/config/initializers/smtp_settings.rb.sample#L13 #### Init script diff --git a/doc/update/9.0-to-9.1.md b/doc/update/9.0-to-9.1.md index 3fd1d023d2a..6f1870a1366 100644 --- a/doc/update/9.0-to-9.1.md +++ b/doc/update/9.0-to-9.1.md @@ -236,7 +236,7 @@ ActionMailer::Base.delivery_method = :smtp See [smtp_settings.rb.sample] as an example. -[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/9-0-stable/config/initializers/smtp_settings.rb.sample#L13 +[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/9-1-stable/config/initializers/smtp_settings.rb.sample#L13 #### Init script diff --git a/doc/update/9.1-to-9.2.md b/doc/update/9.1-to-9.2.md index 5f7a616cc7d..ce72b313031 100644 --- a/doc/update/9.1-to-9.2.md +++ b/doc/update/9.1-to-9.2.md @@ -194,7 +194,7 @@ ActionMailer::Base.delivery_method = :smtp See [smtp_settings.rb.sample] as an example. -[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/9-1-stable/config/initializers/smtp_settings.rb.sample#L13 +[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/9-2-stable/config/initializers/smtp_settings.rb.sample#L13 #### Init script diff --git a/doc/update/9.2-to-9.3.md b/doc/update/9.2-to-9.3.md index 9d0b0da7edb..779ced0cf75 100644 --- a/doc/update/9.2-to-9.3.md +++ b/doc/update/9.2-to-9.3.md @@ -230,7 +230,7 @@ ActionMailer::Base.delivery_method = :smtp See [smtp_settings.rb.sample] as an example. -[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/9-2-stable/config/initializers/smtp_settings.rb.sample#L13 +[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/9-3-stable/config/initializers/smtp_settings.rb.sample#L13 #### Init script diff --git a/doc/update/9.3-to-9.4.md b/doc/update/9.3-to-9.4.md index 9ee01bc9c51..78d8a6c7de5 100644 --- a/doc/update/9.3-to-9.4.md +++ b/doc/update/9.3-to-9.4.md @@ -243,7 +243,7 @@ ActionMailer::Base.delivery_method = :smtp See [smtp_settings.rb.sample] as an example. -[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/9-3-stable/config/initializers/smtp_settings.rb.sample#L13 +[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/9-4-stable/config/initializers/smtp_settings.rb.sample#L13 #### Init script diff --git a/doc/update/9.4-to-9.5.md b/doc/update/9.4-to-9.5.md index 1b5a15589af..a7255142ef5 100644 --- a/doc/update/9.4-to-9.5.md +++ b/doc/update/9.4-to-9.5.md @@ -252,7 +252,7 @@ ActionMailer::Base.delivery_method = :smtp See [smtp_settings.rb.sample] as an example. -[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/9-4-stable/config/initializers/smtp_settings.rb.sample#L13 +[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/9-5-stable/config/initializers/smtp_settings.rb.sample#L13 #### Init script diff --git a/doc/update/9.5-to-10.0.md b/doc/update/9.5-to-10.0.md new file mode 100644 index 00000000000..8581e6511f2 --- /dev/null +++ b/doc/update/9.5-to-10.0.md @@ -0,0 +1,356 @@ +# From 9.5 to 10.0 + +Make sure you view this update guide from the tag (version) of GitLab you would +like to install. In most cases this should be the highest numbered production +tag (without rc in it). You can select the tag in the version dropdown at the +top left corner of GitLab (below the menu bar). + +If the highest number stable branch is unclear please check the +[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation +guide links by version. + +### 1. Stop server + +```bash +sudo service gitlab stop +``` + +### 2. Backup + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production +``` + +### 3. Update Ruby + +NOTE: GitLab 9.0 and higher only support Ruby 2.3.x and dropped support for Ruby 2.1.x. Be +sure to upgrade your interpreter if necessary. + +You can check which version you are running with `ruby -v`. + +Download and compile Ruby: + +```bash +mkdir /tmp/ruby && cd /tmp/ruby +curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.3.tar.gz +echo '1014ee699071aa2ddd501907d18cbe15399c997d ruby-2.3.3.tar.gz' | shasum -c - && tar xzf ruby-2.3.3.tar.gz +cd ruby-2.3.3 +./configure --disable-install-rdoc +make +sudo make install +``` + +Install Bundler: + +```bash +sudo gem install bundler --no-ri --no-rdoc +``` + +### 4. Update Node + +GitLab now runs [webpack](http://webpack.js.org) to compile frontend assets and +it has a minimum requirement of node v4.3.0. + +You can check which version you are running with `node -v`. If you are running +a version older than `v4.3.0` you will need to update to a newer version. You +can find instructions to install from community maintained packages or compile +from source at the nodejs.org website. + +<https://nodejs.org/en/download/> + + +Since 8.17, GitLab requires the use of yarn `>= v0.17.0` to manage +JavaScript dependencies. + +```bash +curl --silent --show-error https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - +echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list +sudo apt-get update +sudo apt-get install yarn +``` + +More information can be found on the [yarn website](https://yarnpkg.com/en/docs/install). + +### 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 + +sudo -u git -H git fetch --all +sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically +sudo -u git -H git checkout -- locale +``` + +For GitLab Community Edition: + +```bash +cd /home/git/gitlab + +sudo -u git -H git checkout 10-0-stable +``` + +OR + +For GitLab Enterprise Edition: + +```bash +cd /home/git/gitlab + +sudo -u git -H git checkout 10-0-stable-ee +``` + +### 7. Update gitlab-shell + +```bash +cd /home/git/gitlab-shell + +sudo -u git -H git fetch --all --tags +sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_SHELL_VERSION) +sudo -u git -H bin/compile +``` + +### 8. Update gitlab-workhorse + +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. + +```bash +cd /home/git/gitlab-workhorse + +sudo -u git -H git fetch --all --tags +sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_WORKHORSE_VERSION) +sudo -u git -H make +``` + +### 9. Update Gitaly + +#### New Gitaly configuration options required + +In order to function Gitaly needs some additional configuration information. Below we assume you installed Gitaly in `/home/git/gitaly` and GitLab Shell in `/home/git/gitlab-shell'. + +```shell +echo ' +[gitaly-ruby] +dir = "/home/git/gitaly/ruby" + +[gitlab-shell] +dir = "/home/git/gitlab-shell" +' | sudo -u git tee -a /home/git/gitaly/config.toml +``` + +#### Check Gitaly configuration + +Due to a bug in the `rake gitlab:gitaly:install` script your Gitaly +configuration file may contain syntax errors. The block name +`[[storages]]`, which may occur more than once in your `config.toml` +file, should be `[[storage]]` instead. + +```shell +sudo -u git -H sed -i.pre-10.0 's/\[\[storages\]\]/[[storage]]/' /home/git/gitaly/config.toml +``` + +#### Compile Gitaly + +```shell +cd /home/git/gitaly +sudo -u git -H git fetch --all --tags +sudo -u git -H git checkout v$(</home/git/gitlab/GITALY_SERVER_VERSION) +sudo -u git -H make +``` + +### 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';" +``` + +If you use MySQL with replication, or just have MySQL configured with binary logging, +you will need to also run the following on all of your MySQL servers: + +```bash +mysql -u root -p -e "SET GLOBAL log_bin_trust_function_creators = 1;" +``` + +You can make this setting permanent by adding it to your `my.cnf`: + +``` +log_bin_trust_function_creators=1 +``` + +### 11. Update configuration files + +#### New configuration options for `gitlab.yml` + +There might be configuration options available for [`gitlab.yml`][yaml]. View them with the command below and apply them manually to your current `gitlab.yml`: + +```sh +cd /home/git/gitlab + +git diff origin/9-5-stable:config/gitlab.yml.example origin/10-0-stable:config/gitlab.yml.example +``` + +#### Nginx configuration + +Ensure you're still up-to-date with the latest NGINX configuration changes: + +```sh +cd /home/git/gitlab + +# For HTTPS configurations +git diff origin/9-5-stable:lib/support/nginx/gitlab-ssl origin/10-0-stable:lib/support/nginx/gitlab-ssl + +# For HTTP configurations +git diff origin/9-5-stable:lib/support/nginx/gitlab origin/10-0-stable:lib/support/nginx/gitlab +``` + +If you are using Strict-Transport-Security in your installation to continue using it you must enable it in your Nginx +configuration as GitLab application no longer handles setting it. + +If you are using Apache instead of NGINX please see the updated [Apache templates]. +Also note that because Apache does not support upstreams behind Unix sockets you +will need to let gitlab-workhorse listen on a TCP port. You can do this +via [/etc/default/gitlab]. + +[Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache +[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/10-0-stable/lib/support/init.d/gitlab.default.example#L38 + +#### SMTP configuration + +If you're installing from source and use SMTP to deliver mail, you will need to add the following line +to config/initializers/smtp_settings.rb: + +```ruby +ActionMailer::Base.delivery_method = :smtp +``` + +See [smtp_settings.rb.sample] as an example. + +[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/10-0-stable/config/initializers/smtp_settings.rb.sample#L13 + +#### Init script + +There might be new configuration options available for [`gitlab.default.example`][gl-example]. View them with the command below and apply them manually to your current `/etc/default/gitlab`: + +```sh +cd /home/git/gitlab + +git diff origin/9-5-stable:lib/support/init.d/gitlab.default.example origin/10-0-stable:lib/support/init.d/gitlab.default.example +``` + +Ensure you're still up-to-date with the latest init script changes: + +```bash +cd /home/git/gitlab + +sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab +``` + +For Ubuntu 16.04.1 LTS: + +```bash +sudo systemctl daemon-reload +``` + +### 12. Install libs, migrations, etc. + +```bash +cd /home/git/gitlab + +# MySQL installations (note: the line below states '--without postgres') +sudo -u git -H bundle install --without postgres development test --deployment + +# PostgreSQL installations (note: the line below states '--without mysql') +sudo -u git -H bundle install --without mysql development test --deployment + +# Optional: clean up old gems +sudo -u git -H bundle clean + +# Run database migrations +sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production + +# Compile GetText PO files + +sudo -u git -H bundle exec rake gettext:compile RAILS_ENV=production + +# Update node dependencies and recompile assets +sudo -u git -H bundle exec rake yarn:install gitlab:assets:clean gitlab:assets:compile RAILS_ENV=production NODE_ENV=production + +# Clean up cache +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). + +### 13. Start application + +```bash +sudo service gitlab start +sudo service nginx restart +``` + +### 14. Check application status + +Check if GitLab and its environment are configured correctly: + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production +``` + +To make sure you didn't miss anything run a more thorough check: + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production +``` + +If all items are green, then congratulations, the upgrade is complete! + +## Things went south? Revert to previous version (9.5) + +### 1. Revert the code to the previous version + +Follow the [upgrade guide from 9.4 to 9.5](9.4-to-9.5.md), except for the +database migration (the backup is already migrated to the previous version). + +### 2. Restore from the backup + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production +``` + +If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above. + +[yaml]: https://gitlab.com/gitlab-org/gitlab-ce/blob/10-0-stable/config/gitlab.yml.example +[gl-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/10-0-stable/lib/support/init.d/gitlab.default.example diff --git a/doc/update/mysql_to_postgresql.md b/doc/update/mysql_to_postgresql.md index a7de5648c0e..5dc8e6f65f8 100644 --- a/doc/update/mysql_to_postgresql.md +++ b/doc/update/mysql_to_postgresql.md @@ -1,3 +1,5 @@ +*** NOTE: These instructions should be considered deprecated. In GitLab 10.0 we will be releasing new migration instructions using [pgloader](http://pgloader.io/). + # Migrating GitLab from MySQL to Postgres *Make sure you view this [guide from the `master` branch](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/update/mysql_to_postgresql.md#migrating-gitlab-from-mysql-to-postgres) for the most up to date instructions.* diff --git a/doc/update/patch_versions.md b/doc/update/patch_versions.md index 30107360446..b2679d1ff22 100644 --- a/doc/update/patch_versions.md +++ b/doc/update/patch_versions.md @@ -74,7 +74,15 @@ cd /home/git/gitlab sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse]" RAILS_ENV=production ``` -### 5. Update gitlab-shell to the corresponding version +### 5. Update gitaly to the corresponding version + +```bash +cd /home/git/gitlab + +sudo -u git -H bundle exec rake "gitlab:gitaly:install[/home/git/gitaly]" RAILS_ENV=production +``` + +### 6. Update gitlab-shell to the corresponding version ```bash cd /home/git/gitlab-shell @@ -84,14 +92,14 @@ sudo -u git -H git checkout v`cat /home/git/gitlab/GITLAB_SHELL_VERSION` -b v`ca sudo -u git -H sh -c 'if [ -x bin/compile ]; then bin/compile; fi' ``` -### 6. Start application +### 7. Start application ```bash sudo service gitlab start sudo service nginx restart ``` -### 7. Check application status +### 8. Check application status Check if GitLab and its environment are configured correctly: diff --git a/doc/user/admin_area/monitoring/convdev.md b/doc/user/admin_area/monitoring/convdev.md index 3d93c7557a4..a98602c4d70 100644 --- a/doc/user/admin_area/monitoring/convdev.md +++ b/doc/user/admin_area/monitoring/convdev.md @@ -23,7 +23,7 @@ If you have just started using GitLab, it may take a few weeks for data to be collected before this feature is available. This feature is accessible only to a system admin, at -**Admin area > Monitoring > ConvDev Index**. +**Admin area > Overview > ConvDev Index**. [ce-30469]: https://gitlab.com/gitlab-org/gitlab-ce/issues/30469 [ping]: ../settings/usage_statistics.md#usage-ping diff --git a/doc/user/admin_area/monitoring/img/convdev_index.png b/doc/user/admin_area/monitoring/img/convdev_index.png Binary files differindex 4e47ff2228d..ffe18d76c96 100644 --- a/doc/user/admin_area/monitoring/img/convdev_index.png +++ b/doc/user/admin_area/monitoring/img/convdev_index.png diff --git a/doc/user/project/integrations/prometheus_library/haproxy.md b/doc/user/project/integrations/prometheus_library/haproxy.md index f2939f047a3..d4b5911a91c 100644 --- a/doc/user/project/integrations/prometheus_library/haproxy.md +++ b/doc/user/project/integrations/prometheus_library/haproxy.md @@ -7,7 +7,7 @@ GitLab has support for automatically detecting and monitoring HAProxy. This is p | Name | Query | | ---- | ----- | -| Throughput (req/sec) | sum(rate(haproxy_frontend_http_requests_total{%{environment_filter}}[2m])) | +| Throughput (req/sec) | sum(rate(haproxy_frontend_http_requests_total{%{environment_filter}}[2m])) by (code) | | HTTP Error Rate (%) | sum(rate(haproxy_frontend_http_requests_total{code="5xx",%{environment_filter}}[2m])) / sum(rate(haproxy_frontend_http_requests_total{%{environment_filter}}[2m])) | ## Configuring Prometheus to monitor for HAProxy metrics diff --git a/doc/user/project/integrations/prometheus_library/kubernetes.md b/doc/user/project/integrations/prometheus_library/kubernetes.md index 9f0308d8111..4d39ae0c4fa 100644 --- a/doc/user/project/integrations/prometheus_library/kubernetes.md +++ b/doc/user/project/integrations/prometheus_library/kubernetes.md @@ -8,7 +8,7 @@ GitLab has support for automatically detecting and monitoring Kubernetes metrics | Name | Query | | ---- | ----- | | Average Memory Usage (MB) | (sum(container_memory_usage_bytes{container_name!="POD",%{environment_filter}}) / count(container_memory_usage_bytes{container_name!="POD",%{environment_filter}})) /1024/1024 | -| Average CPU Utilization (%) | sum(rate(container_cpu_usage_seconds_total{container_name!="POD",%{environment_filter}}[2m])) / count(container_cpu_usage_seconds_total{container_name!="POD",%{environment_filter}}) * 100 | +| Average CPU Utilization (%) | sum(rate(container_cpu_usage_seconds_total{container_name!="POD",%{environment_filter}}[2m])) by (cpu) * 100 | ## Configuring Prometheus to monitor for Kubernetes node metrics diff --git a/doc/user/project/integrations/prometheus_library/nginx.md b/doc/user/project/integrations/prometheus_library/nginx.md index 12e3321f5f3..bab22f9a384 100644 --- a/doc/user/project/integrations/prometheus_library/nginx.md +++ b/doc/user/project/integrations/prometheus_library/nginx.md @@ -7,7 +7,7 @@ GitLab has support for automatically detecting and monitoring NGINX. This is pro | Name | Query | | ---- | ----- | -| Throughput (req/sec) | sum(rate(nginx_requests_total{server_zone!="*", server_zone!="_", %{environment_filter}}[2m])) | +| Throughput (req/sec) | sum(rate(nginx_responses_total{server_zone!="*", server_zone!="_", %{environment_filter}}[2m])) by (status_code) | | Latency (ms) | avg(nginx_upstream_response_msecs_avg{%{environment_filter}}) | | HTTP Error Rate (HTTP Errors / sec) | rate(nginx_responses_total{status_code="5xx", %{environment_filter}}[2m])) | diff --git a/doc/user/project/integrations/prometheus_library/nginx_ingress.md b/doc/user/project/integrations/prometheus_library/nginx_ingress.md index 84ee8bc45e5..17a47cfa646 100644 --- a/doc/user/project/integrations/prometheus_library/nginx_ingress.md +++ b/doc/user/project/integrations/prometheus_library/nginx_ingress.md @@ -7,19 +7,33 @@ GitLab has support for automatically detecting and monitoring the Kubernetes NGI | Name | Query | | ---- | ----- | -| Throughput (req/sec) | sum(rate(nginx_upstream_requests_total{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) | +| Throughput (req/sec) | sum(rate(nginx_upstream_responses_total{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) by (status_code) | | Latency (ms) | avg(nginx_upstream_response_msecs_avg{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}) | | HTTP Error Rate (HTTP Errors / sec) | sum(rate(nginx_upstream_responses_total{status_code="5xx", upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) | -## Configuring Prometheus to monitor for NGINX ingress metrics +## Configuring NGINX ingress monitoring -The easiest way to get started is to use at least version 0.9.0 of [NGINX ingress](https://github.com/kubernetes/ingress/tree/master/controllers/nginx). If you are using NGINX as your Kubernetes ingress, there is [direct support](https://github.com/kubernetes/ingress/pull/423) for enabling Prometheus monitoring in the 0.9.0 release. +If you have deployed with the [gitlab-omnibus](https://docs.gitlab.com/ee/install/kubernetes/gitlab_omnibus.md) Helm chart, and your application is running in the same cluster, no further action is required. The ingress metrics will be automatically enabled and annotated for Prometheus monitoring. Simply ensure Prometheus monitoring is [enabled for your project](../prometheus.md), which is on by default. -If you have deployed with the [gitlab-omnibus](https://docs.gitlab.com/ee/install/kubernetes/gitlab_omnibus.md) Helm chart, these metrics will be automatically enabled and annotated for Prometheus monitoring. +For other deployments, there is some configuration required depending on your installation: +* NGINX Ingress should be version 0.9.0 or above +* NGINX Ingress should be annotated for Prometheus monitoring +* Prometheus should be configured to monitor annotated pods + +### Setting up NGINX Ingress for Prometheus monitoring + +Version 0.9.0 and above of [NGINX ingress](https://github.com/kubernetes/ingress/tree/master/controllers/nginx) have built-in support for exporting Prometheus metrics. To enable, a ConfigMap setting must be passed: `enable-vts-status: "true"`. Once enabled, a Prometheus metrics endpoint will start running on port 10254. + +With metric data now available, Prometheus needs to be configured to collect it. The easiest way to do this is to leverage Prometheus' [built-in Kubernetes service discovery](https://prometheus.io/docs/operating/configuration/#kubernetes_sd_config), which automatically detects a variety of Kubernetes components and makes them available for monitoring. Since NGINX ingress metrics are exposed per pod, a scrape job for Kubernetes pods is required. A sample pod scraping configuration [is available](https://github.com/prometheus/prometheus/blob/master/documentation/examples/prometheus-kubernetes.yml#L248). This configuration will detect pods and enable collection of metrics **only if** they have been specifically annotated for monitoring. + +Depending on how NGINX ingress was deployed, typically a DaemonSet or Deployment, edit the corresponding YML spec. Two new annotations need to be added: +* `prometheus.io/scrape: "true"` +* `prometheus.io/port: "10254"` + +Prometheus should now be collecting NGINX ingress metrics. To validate view the Prometheus Targets, available under `Status > Targets` on the Prometheus dashboard. New entries for NGINX should be listed in the kubernetes pod monitoring job, `kubernetes-pods`. ## Specifying the Environment label -In order to isolate and only display relevant metrics for a given environment -however, GitLab needs a method to detect which labels are associated. To do this, GitLab will search metrics with appropriate labels. In this case, the `upstream` label must be of the form `<Kubernetes Namespace>-<CI_ENVIRONMENT_SLUG>-*`. +In order to isolate and only display relevant metrics for a given environment, GitLab needs a method to detect which labels are associated. To do this, GitLab will search for metrics with appropriate labels. In this case, the `upstream` label must be of the form `<KUBE_NAMESPACE>-<CI_ENVIRONMENT_SLUG>-*`. If you have used [Auto Deploy](https://docs.gitlab.com/ee/ci/autodeploy/index.html) to deploy your app, this format will be used automatically and metrics will be detected with no action on your part. diff --git a/doc/user/project/issues/issues_functionalities.md b/doc/user/project/issues/issues_functionalities.md index 074b2c19c43..66140f389af 100644 --- a/doc/user/project/issues/issues_functionalities.md +++ b/doc/user/project/issues/issues_functionalities.md @@ -167,6 +167,7 @@ Once you wrote your comment, you can either: #### 18. New Merge Request - Create a new merge request (with a new source branch named after the issue) in one action. -The merge request will automatically close that issue as soon as merged. +The merge request will automatically inherit the milestone and labels of the issue. The merge +request will automatically close that issue as soon as merged. - Optionally, you can just create a [new branch](../repository/web_editor.md#create-a-new-branch-from-an-issue) named after that issue. diff --git a/doc/user/project/merge_requests/img/group_merge_requests_list_view.png b/doc/user/project/merge_requests/img/group_merge_requests_list_view.png Binary files differindex 02a88d0112f..7d0756505db 100644 --- a/doc/user/project/merge_requests/img/group_merge_requests_list_view.png +++ b/doc/user/project/merge_requests/img/group_merge_requests_list_view.png diff --git a/doc/user/project/repository/img/compare_branches.png b/doc/user/project/repository/img/compare_branches.png Binary files differindex 353bd72ef4e..d7ab587f030 100755..100644 --- a/doc/user/project/repository/img/compare_branches.png +++ b/doc/user/project/repository/img/compare_branches.png diff --git a/doc/user/search/index.md b/doc/user/search/index.md index 21e96d8b11c..2b23c494dc4 100644 --- a/doc/user/search/index.md +++ b/doc/user/search/index.md @@ -31,8 +31,8 @@ on the search field on the top-right of your screen: If you want to search for issues present in a specific project, navigate to a project's **Issues** tab, and click on the field **Search or filter results...**. It will -display a dropdown menu, from which you can add filters per author, assignee, milestone, label, -and weight. When done, press **Enter** on your keyboard to filter the issues. +display a dropdown menu, from which you can add filters per author, assignee, milestone, +label, weight, and 'my-reaction' (based on your emoji votes). When done, press **Enter** on your keyboard to filter the issues. ![filter issues in a project](img/issue_search_filter.png) @@ -63,8 +63,6 @@ the same way as you do for projects. ![filter issues in a group](img/group_issues_filter.png) The same process is valid for merge requests. Navigate to your project's **Merge Requests** tab. -The search and filter UI currently uses dropdowns. In a future release, the same -dynamic UI as above will be carried over here. ## Search history diff --git a/features/profile/active_tab.feature b/features/profile/active_tab.feature deleted file mode 100644 index 21d7d6c3800..00000000000 --- a/features/profile/active_tab.feature +++ /dev/null @@ -1,29 +0,0 @@ -@profile -Feature: Profile Active Tab - Background: - Given I sign in as a user - - Scenario: On Profile Home - Given I visit profile page - Then the active main tab should be Home - And no other main tabs should be active - - Scenario: On Profile Account - Given I visit profile account page - Then the active main tab should be Account - And no other main tabs should be active - - Scenario: On Profile SSH Keys - Given I visit profile SSH keys page - Then the active main tab should be SSH Keys - And no other main tabs should be active - - Scenario: On Profile Preferences - Given I visit profile preferences page - Then the active main tab should be Preferences - And no other main tabs should be active - - Scenario: On Profile Authentication log - Given I visit Authentication log page - Then the active main tab should be Authentication log - And no other main tabs should be active diff --git a/features/profile/emails.feature b/features/profile/emails.feature deleted file mode 100644 index 19ed949f6ae..00000000000 --- a/features/profile/emails.feature +++ /dev/null @@ -1,26 +0,0 @@ -@profile -Feature: Profile Emails - Background: - Given I sign in as a user - And I visit profile emails page - - Scenario: I should see emails - Then I should see my emails - - Scenario: Add new email - Given I submit new email "my@email.com" - Then I should see new email "my@email.com" - And I should see my emails - - Scenario: Add duplicate email - Given I submit duplicate email @user.email - Then I should not have @user.email added - And I should see my emails - - Scenario: Remove email - Given I submit new email "my@email.com" - Then I should see new email "my@email.com" - And I should see my emails - Then I click link "Remove" for "my@email.com" - Then I should not see email "my@email.com" - And I should see my emails diff --git a/features/project/builds/summary.feature b/features/project/builds/summary.feature deleted file mode 100644 index 3bf15b0cf87..00000000000 --- a/features/project/builds/summary.feature +++ /dev/null @@ -1,30 +0,0 @@ -Feature: Project Builds Summary - Background: - Given I sign in as a user - And I own a project - And project has CI enabled - And project has coverage enabled - And project has a recent build - - @javascript - Scenario: I browse build details page - When I visit recent build details page - Then I see details of a build - And I see build trace - - @javascript - Scenario: I browse project builds page - When I visit project builds page - Then I see coverage - Then I see button to CI Lint - - @javascript - Scenario: I erase a build - Given recent build is successful - And recent build has a build trace - When I visit recent build details page - And I click erase build button - Then recent build has been erased - And recent build summary does not have artifacts widget - And recent build summary contains information saying that build has been erased - And the build count cache is updated diff --git a/features/project/group_links.feature b/features/project/group_links.feature deleted file mode 100644 index 2657c4487ad..00000000000 --- a/features/project/group_links.feature +++ /dev/null @@ -1,16 +0,0 @@ -Feature: Project Group Links - Background: - Given I sign in as a user - And I own project "Shop" - And project "Shop" is shared with group "Ops" - And project "Shop" is not shared with group "Market" - And I visit project group links page - - Scenario: I should see list of groups - Then I should see project already shared with group "Ops" - Then I should see project is not shared with group "Market" - - @javascript - Scenario: I share project with group - When I select group "Market" for share - Then I should see project is shared with group "Market" diff --git a/features/project/issues/award_emoji.feature b/features/project/issues/award_emoji.feature deleted file mode 100644 index 1d7adfdd2c2..00000000000 --- a/features/project/issues/award_emoji.feature +++ /dev/null @@ -1,45 +0,0 @@ -@project_issues -Feature: Award Emoji - Background: - Given I sign in as a user - And I own project "Shop" - And project "Shop" has issue "Bugfix" - And I visit "Bugfix" issue page - - @javascript - Scenario: I repeatedly add and remove thumbsup award in the issue - Given I click the thumbsup award Emoji - Then I have award added - Given I click the thumbsup award Emoji - Then I have no awards added - Given I click the thumbsup award Emoji - Then I have award added - - @javascript - Scenario: I add and remove custom award in the issue - Given I click to emoji-picker - Then The emoji menu is visible - And The search field is focused - Then I click to emoji in the picker - Then I have award added - And I can remove it by clicking to icon - - @javascript - Scenario: I can see the list of emoji categories - Given I click to emoji-picker - Then The emoji menu is visible - And The search field is focused - Then I can see the activity and food categories - - @javascript - Scenario: I can search emoji - Given I click to emoji-picker - Then The emoji menu is visible - And The search field is focused - And I search "hand" - Then I see search result for "hand" - - @javascript - Scenario: I add award emoji using regular comment - Given I leave comment with a single emoji - Then I have new comment with emoji added diff --git a/features/project/merge_requests/accept.feature b/features/project/merge_requests/accept.feature deleted file mode 100644 index 2ab1c19f452..00000000000 --- a/features/project/merge_requests/accept.feature +++ /dev/null @@ -1,28 +0,0 @@ -@project_merge_requests -Feature: Project Merge Requests Acceptance - Background: - Given There is an open Merge Request - And I am signed in as a developer of the project - - @javascript - Scenario: Accepting the Merge Request and removing the source branch - Given I am on the Merge Request detail page - When I check the "Remove source branch" option - And I click on Accept Merge Request - Then I should see merge request merged - And I should not see the Remove Source Branch button - - @javascript - Scenario: Accepting the Merge Request when URL has an anchor - Given I am on the Merge Request detail with note anchor page - When I check the "Remove source branch" option - And I click on Accept Merge Request - Then I should see merge request merged - And I should not see the Remove Source Branch button - - @javascript - Scenario: Accepting the Merge Request without removing the source branch - Given I am on the Merge Request detail page - When I click on Accept Merge Request - Then I should see merge request merged - And I should see the Remove Source Branch button diff --git a/features/project/merge_requests/revert.feature b/features/project/merge_requests/revert.feature deleted file mode 100644 index aaac5fd7209..00000000000 --- a/features/project/merge_requests/revert.feature +++ /dev/null @@ -1,29 +0,0 @@ -@project_merge_requests -Feature: Revert Merge Requests - Background: - Given There is an open Merge Request - And I am signed in as a developer of the project - And I am on the Merge Request detail page - And I click on Accept Merge Request - And I am on the Merge Request detail page - - @javascript - Scenario: I revert a merge request - Given I click on the revert button - And I revert the changes directly - Then I should see the revert merge request notice - - @javascript - Scenario: I revert a merge request that was previously reverted - Given I click on the revert button - And I revert the changes directly - And I am on the Merge Request detail page - And I click on the revert button - And I revert the changes directly - Then I should see a revert error - - @javascript - Scenario: I revert a merge request in a new merge request - Given I click on the revert button - And I revert the changes in a new merge request - Then I should see the new merge request notice diff --git a/features/project/milestone.feature b/features/project/milestone.feature deleted file mode 100644 index 5e7b211fa27..00000000000 --- a/features/project/milestone.feature +++ /dev/null @@ -1,16 +0,0 @@ -Feature: Project Milestone - Background: - Given I sign in as a user - And I own project "Shop" - And project "Shop" has labels: "bug", "feature", "enhancement" - And project "Shop" has milestone "v2.2" - And milestone has issue "Bugfix1" with labels: "bug", "feature" - And milestone has issue "Bugfix2" with labels: "bug", "enhancement" - - @javascript - Scenario: Listing labels from labels tab - Given I visit project "Shop" milestones page - And I click link "v2.2" - And I click link "Labels" - Then I should see the list of labels - And I should see the labels "bug", "enhancement" and "feature" diff --git a/features/project/team_management.feature b/features/project/team_management.feature deleted file mode 100644 index aed41924cd9..00000000000 --- a/features/project/team_management.feature +++ /dev/null @@ -1,26 +0,0 @@ -Feature: Project Team Management - Background: - Given I sign in as a user - And I own project "Shop" - And gitlab user "Mike" - And gitlab user "Dmitriy" - And "Dmitriy" is "Shop" developer - And I visit project "Shop" team page - - Scenario: Cancel team member - Given I click cancel link for "Dmitriy" - Then I visit project "Shop" team page - And I should not see "Dmitriy" in team list - - Scenario: Import team from another project - Given I own project "Website" - And "Mike" is "Website" reporter - When I visit project "Shop" team page - And I click link "Import team from another project" - And I submit "Website" project for import team - Then I should see "Mike" in team list as "Reporter" - - Scenario: See all members of projects shared group - Given I share project with group "OpenSource" - And I visit project "Shop" team page - Then I should see "Opensource" group user listing diff --git a/features/project/wiki.feature b/features/project/wiki.feature deleted file mode 100644 index a04228de03b..00000000000 --- a/features/project/wiki.feature +++ /dev/null @@ -1,101 +0,0 @@ -Feature: Project Wiki - Background: - Given I sign in as a user - And I own project "Shop" - Given I visit project wiki page - - Scenario: Add new page - Given I create the Wiki Home page - Then I should see the newly created wiki page - - Scenario: Add new page with errors - Given I create the Wiki Home page with no content - Then I should see a "Content can't be blank" error message - When I create the Wiki Home page - Then I should see the newly created wiki page - - Scenario: Pressing Cancel while editing a brand new Wiki - Given I click on the Cancel button - Then I should be redirected back to the Edit Home Wiki page - - Scenario: Edit existing page - Given I have an existing Wiki page - And I browse to that Wiki page - And I click on the Edit button - And I change the content - Then I should see the updated content - - Scenario: Pressing Cancel while editing an existing Wiki page - Given I have an existing Wiki page - And I browse to that Wiki page - And I click on the Edit button - And I click on the Cancel button - Then I should be redirected back to that Wiki page - - Scenario: View page history - Given I have an existing wiki page - And That page has two revisions - And I browse to that Wiki page - And I click the History button - Then I should see both revisions - - Scenario: Destroy Wiki page - Given I have an existing wiki page - And I browse to that Wiki page - And I click on the Edit button - And I click on the "Delete this page" button - Then The page should be deleted - - Scenario: View all pages - Given I have an existing wiki page - And I browse to that Wiki page - Then I should see the existing page in the pages list - - Scenario: File exists in wiki repo - Given I have an existing Wiki page with images linked on page - And I browse to wiki page with images - And I click on existing image link - Then I should see the image from wiki repo - - Scenario: Image in wiki repo shown on the page - Given I have an existing Wiki page with images linked on page - And I browse to wiki page with images - Then Image should be shown on the page - - Scenario: File does not exist in wiki repo - Given I have an existing Wiki page with images linked on page - And I browse to wiki page with images - And I click on image link - Then I should see the new wiki page form - - @javascript - Scenario: New Wiki page that has a path - Given I create a New page with paths - Then I should see non-escaped link in the pages list - - @javascript - Scenario: Edit Wiki page that has a path - Given I create a New page with paths - And I edit the Wiki page with a path - Then I should see a non-escaped path - And I should see the Editing page - And I change the content - Then I should see the updated content - - @javascript - Scenario: View the page history of a Wiki page that has a path - Given I create a New page with paths - And I view the page history of a Wiki page that has a path - Then I should see a non-escaped path - And I should see the page history - - @javascript - Scenario: View an old page version of a Wiki page - Given I create a New page with paths - And I edit the Wiki page with a path - Then I should see a non-escaped path - And I should see the Editing page - And I change the content - Then I click on Page History - And I should see the page history - And I should see a link with a version ID diff --git a/features/steps/profile/active_tab.rb b/features/steps/profile/active_tab.rb deleted file mode 100644 index 069d4e6a23d..00000000000 --- a/features/steps/profile/active_tab.rb +++ /dev/null @@ -1,25 +0,0 @@ -class Spinach::Features::ProfileActiveTab < Spinach::FeatureSteps - include SharedAuthentication - include SharedPaths - include SharedActiveTab - - step 'the active main tab should be Home' do - ensure_active_main_tab('Profile') - end - - step 'the active main tab should be Account' do - ensure_active_main_tab('Account') - end - - step 'the active main tab should be SSH Keys' do - ensure_active_main_tab('SSH Keys') - end - - step 'the active main tab should be Preferences' do - ensure_active_main_tab('Preferences') - end - - step 'the active main tab should be Authentication log' do - ensure_active_main_tab('Authentication log') - end -end diff --git a/features/steps/profile/emails.rb b/features/steps/profile/emails.rb deleted file mode 100644 index 4f44f932a6d..00000000000 --- a/features/steps/profile/emails.rb +++ /dev/null @@ -1,48 +0,0 @@ -class Spinach::Features::ProfileEmails < Spinach::FeatureSteps - include SharedAuthentication - - step 'I visit profile emails page' do - visit profile_emails_path - end - - step 'I should see my emails' do - expect(page).to have_content(@user.email) - @user.emails.each do |email| - expect(page).to have_content(email.email) - end - end - - step 'I submit new email "my@email.com"' do - fill_in "email_email", with: "my@email.com" - click_button "Add" - end - - step 'I should see new email "my@email.com"' do - email = @user.emails.find_by(email: "my@email.com") - expect(email).not_to be_nil - expect(page).to have_content("my@email.com") - end - - step 'I should not see email "my@email.com"' do - email = @user.emails.find_by(email: "my@email.com") - expect(email).to be_nil - expect(page).not_to have_content("my@email.com") - end - - step 'I click link "Remove" for "my@email.com"' do - # there should only be one remove button at this time - click_link "Remove" - # force these to reload as they have been cached - @user.emails.reload - end - - step 'I submit duplicate email @user.email' do - fill_in "email_email", with: @user.email - click_button "Add" - end - - step 'I should not have @user.email added' do - email = @user.emails.find_by(email: @user.email) - expect(email).to be_nil - end -end diff --git a/features/steps/project/builds/summary.rb b/features/steps/project/builds/summary.rb deleted file mode 100644 index 20a5c873ecd..00000000000 --- a/features/steps/project/builds/summary.rb +++ /dev/null @@ -1,43 +0,0 @@ -class Spinach::Features::ProjectBuildsSummary < Spinach::FeatureSteps - include SharedAuthentication - include SharedProject - include SharedBuilds - include RepoHelpers - - step 'I see coverage' do - page.within('td.coverage') do - expect(page).to have_content "99.9%" - end - end - - step 'I see button to CI Lint' do - page.within('.nav-controls') do - ci_lint_tool_link = page.find_link('CI lint') - expect(ci_lint_tool_link[:href]).to end_with(ci_lint_path) - end - end - - step 'I click erase build button' do - click_link 'Erase' - end - - step 'recent build has been erased' do - expect(@build).not_to have_trace - expect(@build.artifacts_file.exists?).to be_falsy - expect(@build.artifacts_metadata.exists?).to be_falsy - end - - step 'recent build summary does not have artifacts widget' do - expect(page).to have_no_css('.artifacts') - end - - step 'recent build summary contains information saying that build has been erased' do - page.within('.erased') do - expect(page).to have_content 'Job has been erased' - end - end - - step 'the build count cache is updated' do - expect(@build.project.running_or_pending_build_count).to eq @build.project.builds.running_or_pending.count(:all) - end -end diff --git a/features/steps/project/issues/award_emoji.rb b/features/steps/project/issues/award_emoji.rb deleted file mode 100644 index bbd284b4633..00000000000 --- a/features/steps/project/issues/award_emoji.rb +++ /dev/null @@ -1,107 +0,0 @@ -class Spinach::Features::AwardEmoji < Spinach::FeatureSteps - include SharedAuthentication - include SharedProject - include SharedPaths - include Select2Helper - - step 'I visit "Bugfix" issue page' do - visit project_issue_path(@project, @issue) - end - - step 'I click the thumbsup award Emoji' do - page.within '.awards' do - thumbsup = page.first('.award-control') - thumbsup.click - thumbsup.hover - end - end - - step 'I click to emoji-picker' do - page.within '.awards' do - page.find('.js-add-award').click - end - end - - step 'I click to emoji in the picker' do - page.within '.emoji-menu-content' do - emoji_button = page.first('.js-emoji-btn') - emoji_button.hover - emoji_button.click - end - end - - step 'I can remove it by clicking to icon' do - page.within '.awards' do - expect do - page.find('.js-emoji-btn.active').click - wait_for_requests - end.to change { page.all(".award-control.js-emoji-btn").size }.from(3).to(2) - end - end - - step 'I can see the activity and food categories' do - page.within '.emoji-menu' do - expect(page).not_to have_selector 'Activity' - expect(page).not_to have_selector 'Food' - end - end - - step 'I have new comment with emoji added' do - expect(page).to have_selector 'gl-emoji[data-name="smile"]' - end - - step 'I have award added' do - page.within '.awards' do - expect(page).to have_selector '.js-emoji-btn' - expect(page.find('.js-emoji-btn.active .js-counter')).to have_content '1' - expect(page).to have_css(".js-emoji-btn.active[data-original-title='You']") - end - end - - step 'I have no awards added' do - page.within '.awards' do - expect(page).to have_selector '.award-control.js-emoji-btn' - expect(page.all('.award-control.js-emoji-btn').size).to eq(2) - - # Check tooltip data - page.all('.award-control.js-emoji-btn').each do |element| - expect(element['title']).to eq("") - end - - page.all('.award-control .js-counter').each do |element| - expect(element).to have_content '0' - end - end - end - - step 'project "Shop" has issue "Bugfix"' do - @project = Project.find_by(name: 'Shop') - @issue = create(:issue, title: 'Bugfix', project: project) - end - - step 'I leave comment with a single emoji' do - page.within('.js-main-target-form') do - fill_in 'note[note]', with: ':smile:' - click_button 'Comment' - end - end - - step 'I search "hand"' do - fill_in 'emoji-menu-search', with: 'hand' - end - - step 'I see search result for "hand"' do - page.within '.emoji-menu-content' do - expect(page).to have_selector '[data-name="raised_hand"]' - end - end - - step 'The emoji menu is visible' do - page.find(".emoji-menu.is-visible") - end - - step 'The search field is focused' do - expect(page).to have_selector('.js-emoji-menu-search') - expect(page.evaluate_script("document.activeElement.classList.contains('js-emoji-menu-search')")).to eq(true) - end -end diff --git a/features/steps/project/merge_requests/acceptance.rb b/features/steps/project/merge_requests/acceptance.rb deleted file mode 100644 index 3c640e3512a..00000000000 --- a/features/steps/project/merge_requests/acceptance.rb +++ /dev/null @@ -1,55 +0,0 @@ -class Spinach::Features::ProjectMergeRequestsAcceptance < Spinach::FeatureSteps - include LoginHelpers - include WaitForRequests - - step 'I am on the Merge Request detail page' do - visit merge_request_path(@merge_request) - end - - step 'I am on the Merge Request detail with note anchor page' do - visit merge_request_path(@merge_request, anchor: 'note_123') - end - - step 'I uncheck the "Remove source branch" option' do - uncheck('Remove source branch') - end - - step 'I check the "Remove source branch" option' do - check('Remove source branch') - end - - step 'I click on Accept Merge Request' do - click_button('Merge') - end - - step 'I should see the Remove Source Branch button' do - expect(page).to have_selector('.js-remove-branch-button') - - # Wait for View Resource requests to complete so they don't blow up if they are - # only handled after `DatabaseCleaner` has already run - wait_for_requests - end - - step 'I should not see the Remove Source Branch button' do - expect(page).not_to have_selector('.js-remove-branch-button') - - # Wait for View Resource requests to complete so they don't blow up if they are - # only handled after `DatabaseCleaner` has already run - wait_for_requests - end - - step 'There is an open Merge Request' do - @user = create(:user) - @project = create(:project, :public, :repository) - @project_member = create(:project_member, :developer, user: @user, project: @project) - @merge_request = create(:merge_request, :with_diffs, :simple, source_project: @project) - end - - step 'I am signed in as a developer of the project' do - sign_in(@user) - end - - step 'I should see merge request merged' do - expect(page).to have_content('The changes were merged into') - end -end diff --git a/features/steps/project/merge_requests/revert.rb b/features/steps/project/merge_requests/revert.rb deleted file mode 100644 index 25ccf5ab180..00000000000 --- a/features/steps/project/merge_requests/revert.rb +++ /dev/null @@ -1,56 +0,0 @@ -class Spinach::Features::RevertMergeRequests < Spinach::FeatureSteps - include LoginHelpers - include WaitForRequests - - step 'I click on the revert button' do - find("a[href='#modal-revert-commit']").click - end - - step 'I revert the changes directly' do - page.within('#modal-revert-commit') do - uncheck 'create_merge_request' - click_button 'Revert' - end - end - - step 'I should see the revert merge request notice' do - page.should have_content('The merge request has been successfully reverted.') - wait_for_requests - end - - step 'I should not see the revert button' do - expect(page).not_to have_selector(:xpath, "a[href='#modal-revert-commit']") - end - - step 'I am on the Merge Request detail page' do - visit merge_request_path(@merge_request) - end - - step 'I click on Accept Merge Request' do - click_button('Merge') - end - - step 'I am signed in as a developer of the project' do - @user = create(:user) { |u| @project.add_developer(u) } - sign_in(@user) - end - - step 'There is an open Merge Request' do - @merge_request = create(:merge_request, :with_diffs, :simple) - @project = @merge_request.source_project - end - - step 'I should see a revert error' do - page.should have_content('Sorry, we cannot revert this merge request automatically.') - end - - step 'I revert the changes in a new merge request' do - page.within('#modal-revert-commit') do - click_button 'Revert' - end - end - - step 'I should see the new merge request notice' do - page.should have_content('The merge request has been successfully reverted. You can now submit a merge request to get this change into the original branch.') - end -end diff --git a/features/steps/project/project_group_links.rb b/features/steps/project/project_group_links.rb deleted file mode 100644 index 47ee31786a6..00000000000 --- a/features/steps/project/project_group_links.rb +++ /dev/null @@ -1,51 +0,0 @@ -class Spinach::Features::ProjectGroupLinks < Spinach::FeatureSteps - include SharedAuthentication - include SharedProject - include SharedPaths - include Select2Helper - - step 'I should see project already shared with group "Ops"' do - page.within '.project-members-groups' do - expect(page).to have_content "Ops" - end - end - - step 'I should see project is not shared with group "Market"' do - page.within '.project-members-groups' do - expect(page).not_to have_content "Market" - end - end - - step 'I select group "Market" for share' do - click_link 'Share with group' - group = Group.find_by(path: 'market') - select2(group.id, from: "#link_group_id") - select "Master", from: 'link_group_access' - click_button "Share" - end - - step 'I should see project is shared with group "Market"' do - page.within '.project-members-groups' do - expect(page).to have_content "Market" - end - end - - step 'project "Shop" is shared with group "Ops"' do - group = create(:group, name: 'Ops') - share_link = project.project_group_links.new(group_access: Gitlab::Access::MASTER) - share_link.group_id = group.id - share_link.save! - end - - step 'project "Shop" is not shared with group "Market"' do - create(:group, name: 'Market', path: 'market') - end - - step 'I visit project group links page' do - visit project_group_links_path(project) - end - - def project - @project ||= Project.find_by_name "Shop" - end -end diff --git a/features/steps/project/project_milestone.rb b/features/steps/project/project_milestone.rb deleted file mode 100644 index b2d08515e77..00000000000 --- a/features/steps/project/project_milestone.rb +++ /dev/null @@ -1,62 +0,0 @@ -class Spinach::Features::ProjectMilestone < Spinach::FeatureSteps - include SharedAuthentication - include SharedProject - include SharedPaths - include WaitForRequests - - step 'milestone has issue "Bugfix1" with labels: "bug", "feature"' do - project = Project.find_by(name: "Shop") - milestone = project.milestones.find_by(title: 'v2.2') - issue = create(:issue, title: "Bugfix1", project: project, milestone: milestone) - issue.labels << project.labels.find_by(title: 'bug') - issue.labels << project.labels.find_by(title: 'feature') - end - - step 'milestone has issue "Bugfix2" with labels: "bug", "enhancement"' do - project = Project.find_by(name: "Shop") - milestone = project.milestones.find_by(title: 'v2.2') - issue = create(:issue, title: "Bugfix2", project: project, milestone: milestone) - issue.labels << project.labels.find_by(title: 'bug') - issue.labels << project.labels.find_by(title: 'enhancement') - end - - step 'project "Shop" has milestone "v2.2"' do - project = Project.find_by(name: "Shop") - milestone = create(:milestone, - title: "v2.2", - project: project, - description: "# Description header" - ) - 3.times { create(:issue, project: project, milestone: milestone) } - end - - step 'I should see the list of labels' do - expect(page).to have_selector('ul.manage-labels-list') - end - - step 'I should see the labels "bug", "enhancement" and "feature"' do - wait_for_requests - - page.within('#tab-issues') do - expect(page).to have_content 'bug' - expect(page).to have_content 'enhancement' - expect(page).to have_content 'feature' - end - end - - step 'I should see the "bug" label listed only once' do - page.within('#tab-labels') do - expect(page).to have_content('bug', count: 1) - end - end - - step 'I click link "v2.2"' do - click_link "v2.2" - end - - step 'I click link "Labels"' do - page.within('.nav-sidebar') do - page.find(:xpath, "//a[@href='#tab-labels']").click - end - end -end diff --git a/features/steps/project/team_management.rb b/features/steps/project/team_management.rb deleted file mode 100644 index 5c4025ace34..00000000000 --- a/features/steps/project/team_management.rb +++ /dev/null @@ -1,87 +0,0 @@ -class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps - include SharedAuthentication - include SharedProject - include SharedPaths - include Select2Helper - - step 'I should not see "Dmitriy" in team list' do - user = User.find_by(name: "Dmitriy") - expect(page).not_to have_content(user.name) - expect(page).not_to have_content(user.username) - end - - step 'I should see "Mike" in team list as "Reporter"' do - user = User.find_by(name: 'Mike') - project_member = project.project_members.find_by(user_id: user.id) - page.within "#project_member_#{project_member.id}" do - expect(page).to have_content('Mike') - expect(page).to have_content('Reporter') - end - end - - step 'gitlab user "Mike"' do - create(:user, name: "Mike") - end - - step 'gitlab user "Dmitriy"' do - create(:user, name: "Dmitriy") - end - - step '"Dmitriy" is "Shop" developer' do - user = User.find_by(name: "Dmitriy") - project = Project.find_by(name: "Shop") - project.team << [user, :developer] - end - - step 'I own project "Website"' do - @project = create(:project, name: "Website", namespace: @user.namespace) - @project.team << [@user, :master] - end - - step '"Mike" is "Website" reporter' do - user = User.find_by(name: "Mike") - project = Project.find_by(name: "Website") - project.team << [user, :reporter] - end - - step 'I click link "Import team from another project"' do - page.within '.users-project-form' do - click_link "Import" - end - end - - When 'I submit "Website" project for import team' do - project = Project.find_by(name: "Website") - select project.name_with_namespace, from: 'source_project_id' - click_button 'Import' - end - - step 'I click cancel link for "Dmitriy"' do - project = Project.find_by(name: "Shop") - user = User.find_by(name: 'Dmitriy') - project_member = project.project_members.find_by(user_id: user.id) - page.within "#project_member_#{project_member.id}" do - click_link('Remove user from project') - end - end - - step 'I share project with group "OpenSource"' do - project = Project.find_by(name: 'Shop') - os_group = create(:group, name: 'OpenSource') - create(:project, group: os_group) - @os_user1 = create(:user) - @os_user2 = create(:user) - os_group.add_owner(@os_user1) - os_group.add_user(@os_user2, Gitlab::Access::DEVELOPER) - share_link = project.project_group_links.new(group_access: Gitlab::Access::MASTER) - share_link.group_id = os_group.id - share_link.save! - end - - step 'I should see "Opensource" group user listing' do - page.within '.project-members-groups' do - expect(page).to have_content('OpenSource') - expect(first('.group_member')).to have_content('Master') - end - end -end diff --git a/features/steps/project/wiki.rb b/features/steps/project/wiki.rb deleted file mode 100644 index 855757e34b3..00000000000 --- a/features/steps/project/wiki.rb +++ /dev/null @@ -1,195 +0,0 @@ -class Spinach::Features::ProjectWiki < Spinach::FeatureSteps - include SharedAuthentication - include SharedProject - include SharedNote - include SharedPaths - - step 'I click on the Cancel button' do - page.within(:css, ".wiki-form .form-actions") do - click_on "Cancel" - end - end - - step 'I should be redirected back to the Edit Home Wiki page' do - expect(current_path).to eq project_wiki_path(project, :home) - end - - step 'I create the Wiki Home page' do - fill_in "wiki_content", with: '[link test](test)' - page.within '.wiki-form' do - click_on "Create page" - end - end - - step 'I create the Wiki Home page with no content' do - fill_in "wiki_content", with: '' - page.within '.wiki-form' do - click_on "Create page" - end - end - - step 'I should see the newly created wiki page' do - expect(page).to have_content "Home" - expect(page).to have_content "link test" - - click_link "link test" - expect(page).to have_content "Create page" - end - - step 'I have an existing Wiki page' do - wiki.create_page("existing", "content", :markdown, "first commit") - @page = wiki.find_page("existing") - end - - step 'I browse to that Wiki page' do - visit project_wiki_path(project, @page) - end - - step 'I click on the Edit button' do - click_on "Edit" - end - - step 'I change the content' do - fill_in "Content", with: 'Updated Wiki Content' - click_on "Save changes" - end - - step 'I should see the updated content' do - expect(page).to have_content "Updated Wiki Content" - end - - step 'I should be redirected back to that Wiki page' do - expect(current_path).to eq project_wiki_path(project, @page) - end - - step 'That page has two revisions' do - @page.update(content: "new content", message: "second commit") - end - - step 'I click the History button' do - click_on 'Page history' - end - - step 'I should see both revisions' do - expect(page).to have_content current_user.name - expect(page).to have_content "first commit" - expect(page).to have_content "second commit" - end - - step 'I click on the "Delete this page" button' do - click_on "Delete" - end - - step 'The page should be deleted' do - expect(page).to have_content "Page was successfully deleted" - end - - step 'I should see the existing page in the pages list' do - expect(page).to have_content current_user.name - expect(find('.wiki-pages')).to have_content @page.title.capitalize - end - - step 'I have an existing Wiki page with images linked on page' do - wiki.create_page("pictures", "Look at this [image](image.jpg)\n\n ![alt text](image.jpg)", :markdown, "first commit") - @wiki_page = wiki.find_page("pictures") - end - - step 'I browse to wiki page with images' do - visit project_wiki_path(project, @wiki_page) - end - - step 'I click on existing image link' do - file = Gollum::File.new(wiki.wiki) - Gollum::Wiki.any_instance.stub(:file).with("image.jpg", "master", true).and_return(file) - Gollum::File.any_instance.stub(:mime_type).and_return("image/jpeg") - expect(page).to have_link('image', href: "#{wiki.wiki_base_path}/image.jpg") - click_on "image" - end - - step 'I should see the image from wiki repo' do - expect(current_path).to match('wikis/image.jpg') - expect(page).not_to have_xpath('/html') # Page should render the image which means there is no html involved - Gollum::Wiki.any_instance.unstub(:file) - Gollum::File.any_instance.unstub(:mime_type) - end - - step 'Image should be shown on the page' do - expect(page).to have_xpath("//img[@data-src=\"image.jpg\"]") - end - - step 'I click on image link' do - expect(page).to have_link('image', href: "#{wiki.wiki_base_path}/image.jpg") - click_on "image" - end - - step 'I should see the new wiki page form' do - expect(current_path).to match('wikis/image.jpg') - expect(page).to have_content('New Wiki Page') - expect(page).to have_content('Create page') - end - - step 'I create a New page with paths' do - click_on 'New page' - fill_in 'Page slug', with: 'one/two/three-test' - page.within '#modal-new-wiki' do - click_on 'Create page' - end - fill_in "wiki_content", with: 'wiki content' - page.within '.wiki-form' do - click_on "Create page" - end - expect(current_path).to include 'one/two/three-test' - end - - step 'I should see non-escaped link in the pages list' do - expect(page).to have_xpath("//a[@href='/#{project.full_path}/wikis/one/two/three-test']") - end - - step 'I edit the Wiki page with a path' do - expect(find('.wiki-pages')).to have_content('Three') - click_on 'Three' - expect(find('.nav-text')).to have_content('Three') - click_on 'Edit' - end - - step 'I should see a non-escaped path' do - expect(current_path).to include 'one/two/three-test' - end - - step 'I should see the Editing page' do - expect(page).to have_content('Edit Page') - end - - step 'I view the page history of a Wiki page that has a path' do - click_on 'Three' - click_on 'Page history' - end - - step 'I click on Page History' do - click_on 'Page history' - end - - step 'I should see the page history' do - page.within(:css, ".nav-text") do - expect(page).to have_content('History') - end - end - - step 'I search for Wiki content' do - fill_in "Search", with: "wiki_content" - click_button "Search" - end - - step 'I should see a link with a version ID' do - find('a[href*="?version_id"]') - end - - step 'I should see a "Content can\'t be blank" error message' do - expect(page).to have_content('The form contains the following error:') - expect(page).to have_content('Content can\'t be blank') - end - - def wiki - @project_wiki = ProjectWiki.new(project, current_user) - end -end diff --git a/lib/api/api.rb b/lib/api/api.rb index ee4e1688e12..79e55a2f4f7 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -8,7 +8,6 @@ module API logger: Logger.new(LOG_FILENAME), formatter: Gitlab::GrapeLogging::Formatters::LogrageWithTimestamp.new, include: [ - GrapeLogging::Loggers::Response.new, GrapeLogging::Loggers::FilterParameters.new, GrapeLogging::Loggers::ClientEnv.new ] diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 216408064d1..52c49e5caa9 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -45,7 +45,7 @@ module API expose :confirmed_at expose :last_activity_on expose :email - expose :color_scheme_id, :projects_limit, :current_sign_in_at + expose :theme_id, :color_scheme_id, :projects_limit, :current_sign_in_at expose :identities, using: Entities::Identity expose :can_create_group?, as: :can_create_group expose :can_create_project?, as: :can_create_project diff --git a/lib/api/lint.rb b/lib/api/lint.rb index ae43a4a3237..d202eaa4c49 100644 --- a/lib/api/lint.rb +++ b/lib/api/lint.rb @@ -6,7 +6,7 @@ module API requires :content, type: String, desc: 'Content of .gitlab-ci.yml' end post '/lint' do - error = Ci::GitlabCiYamlProcessor.validation_message(params[:content]) + error = Gitlab::Ci::YamlProcessor.validation_message(params[:content]) status 200 diff --git a/lib/api/users.rb b/lib/api/users.rb index 1825c90a23b..bdebda58d3f 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -88,7 +88,7 @@ module API user = User.find_by(id: params[:id]) not_found!('User') unless user && can?(current_user, :read_user, user) - opts = current_user&.admin? ? { with: Entities::UserWithAdmin } : {} + opts = current_user&.admin? ? { with: Entities::UserWithAdmin } : { with: Entities::User } present user, opts end diff --git a/lib/banzai/filter/image_lazy_load_filter.rb b/lib/banzai/filter/image_lazy_load_filter.rb index bcb4f332267..4cd9b02b76c 100644 --- a/lib/banzai/filter/image_lazy_load_filter.rb +++ b/lib/banzai/filter/image_lazy_load_filter.rb @@ -1,6 +1,7 @@ module Banzai module Filter - # HTML filter that moves the value of the src attribute to the data-src attribute so it can be lazy loaded + # HTML filter that moves the value of image `src` attributes to `data-src` + # so they can be lazy loaded. class ImageLazyLoadFilter < HTML::Pipeline::Filter def call doc.xpath('descendant-or-self::img').each do |img| diff --git a/lib/banzai/pipeline/email_pipeline.rb b/lib/banzai/pipeline/email_pipeline.rb index e47c384afc1..8f5f144d582 100644 --- a/lib/banzai/pipeline/email_pipeline.rb +++ b/lib/banzai/pipeline/email_pipeline.rb @@ -1,6 +1,12 @@ module Banzai module Pipeline class EmailPipeline < FullPipeline + def self.filters + super.tap do |filter_array| + filter_array.delete(Banzai::Filter::ImageLazyLoadFilter) + end + end + def self.transform_context(context) super(context).merge( only_path: false diff --git a/lib/ci/ansi2html.rb b/lib/ci/ansi2html.rb deleted file mode 100644 index b9e9f9f7f4a..00000000000 --- a/lib/ci/ansi2html.rb +++ /dev/null @@ -1,331 +0,0 @@ -# ANSI color library -# -# Implementation per http://en.wikipedia.org/wiki/ANSI_escape_code -module Ci - module Ansi2html - # keys represent the trailing digit in color changing command (30-37, 40-47, 90-97. 100-107) - COLOR = { - 0 => 'black', # not that this is gray in the intense color table - 1 => 'red', - 2 => 'green', - 3 => 'yellow', - 4 => 'blue', - 5 => 'magenta', - 6 => 'cyan', - 7 => 'white', # not that this is gray in the dark (aka default) color table - }.freeze - - STYLE_SWITCHES = { - bold: 0x01, - italic: 0x02, - underline: 0x04, - conceal: 0x08, - cross: 0x10 - }.freeze - - def self.convert(ansi, state = nil) - Converter.new.convert(ansi, state) - end - - class Converter - def on_0(s) reset() end - - def on_1(s) enable(STYLE_SWITCHES[:bold]) end - - def on_3(s) enable(STYLE_SWITCHES[:italic]) end - - def on_4(s) enable(STYLE_SWITCHES[:underline]) end - - def on_8(s) enable(STYLE_SWITCHES[:conceal]) end - - def on_9(s) enable(STYLE_SWITCHES[:cross]) end - - def on_21(s) disable(STYLE_SWITCHES[:bold]) end - - def on_22(s) disable(STYLE_SWITCHES[:bold]) end - - def on_23(s) disable(STYLE_SWITCHES[:italic]) end - - def on_24(s) disable(STYLE_SWITCHES[:underline]) end - - def on_28(s) disable(STYLE_SWITCHES[:conceal]) end - - def on_29(s) disable(STYLE_SWITCHES[:cross]) end - - def on_30(s) set_fg_color(0) end - - def on_31(s) set_fg_color(1) end - - def on_32(s) set_fg_color(2) end - - def on_33(s) set_fg_color(3) end - - def on_34(s) set_fg_color(4) end - - def on_35(s) set_fg_color(5) end - - def on_36(s) set_fg_color(6) end - - def on_37(s) set_fg_color(7) end - - def on_38(s) set_fg_color_256(s) end - - def on_39(s) set_fg_color(9) end - - def on_40(s) set_bg_color(0) end - - def on_41(s) set_bg_color(1) end - - def on_42(s) set_bg_color(2) end - - def on_43(s) set_bg_color(3) end - - def on_44(s) set_bg_color(4) end - - def on_45(s) set_bg_color(5) end - - def on_46(s) set_bg_color(6) end - - def on_47(s) set_bg_color(7) end - - def on_48(s) set_bg_color_256(s) end - - def on_49(s) set_bg_color(9) end - - def on_90(s) set_fg_color(0, 'l') end - - def on_91(s) set_fg_color(1, 'l') end - - def on_92(s) set_fg_color(2, 'l') end - - def on_93(s) set_fg_color(3, 'l') end - - def on_94(s) set_fg_color(4, 'l') end - - def on_95(s) set_fg_color(5, 'l') end - - def on_96(s) set_fg_color(6, 'l') end - - def on_97(s) set_fg_color(7, 'l') end - - def on_99(s) set_fg_color(9, 'l') end - - def on_100(s) set_bg_color(0, 'l') end - - def on_101(s) set_bg_color(1, 'l') end - - def on_102(s) set_bg_color(2, 'l') end - - def on_103(s) set_bg_color(3, 'l') end - - def on_104(s) set_bg_color(4, 'l') end - - def on_105(s) set_bg_color(5, 'l') end - - def on_106(s) set_bg_color(6, 'l') end - - def on_107(s) set_bg_color(7, 'l') end - - def on_109(s) set_bg_color(9, 'l') end - - attr_accessor :offset, :n_open_tags, :fg_color, :bg_color, :style_mask - - STATE_PARAMS = [:offset, :n_open_tags, :fg_color, :bg_color, :style_mask].freeze - - def convert(stream, new_state) - reset_state - restore_state(new_state, stream) if new_state.present? - - append = false - truncated = false - - cur_offset = stream.tell - if cur_offset > @offset - @offset = cur_offset - truncated = true - else - stream.seek(@offset) - append = @offset > 0 - end - start_offset = @offset - - open_new_tag - - stream.each_line do |line| - s = StringScanner.new(line) - until s.eos? - if s.scan(/\e([@-_])(.*?)([@-~])/) - handle_sequence(s) - elsif s.scan(/\e(([@-_])(.*?)?)?$/) - break - elsif s.scan(/</) - @out << '<' - elsif s.scan(/\r?\n/) - @out << '<br>' - else - @out << s.scan(/./m) - end - @offset += s.matched_size - end - end - - close_open_tags() - - OpenStruct.new( - html: @out.force_encoding(Encoding.default_external), - state: state, - append: append, - truncated: truncated, - offset: start_offset, - size: stream.tell - start_offset, - total: stream.size - ) - end - - def handle_sequence(s) - indicator = s[1] - commands = s[2].split ';' - terminator = s[3] - - # We are only interested in color and text style changes - triggered by - # sequences starting with '\e[' and ending with 'm'. Any other control - # sequence gets stripped (including stuff like "delete last line") - return unless indicator == '[' && terminator == 'm' - - close_open_tags() - - if commands.empty?() - reset() - return - end - - evaluate_command_stack(commands) - - open_new_tag - end - - def evaluate_command_stack(stack) - return unless command = stack.shift() - - if self.respond_to?("on_#{command}", true) - self.__send__("on_#{command}", stack) # rubocop:disable GitlabSecurity/PublicSend - end - - evaluate_command_stack(stack) - end - - def open_new_tag - css_classes = [] - - unless @fg_color.nil? - fg_color = @fg_color - # Most terminals show bold colored text in the light color variant - # Let's mimic that here - if @style_mask & STYLE_SWITCHES[:bold] != 0 - fg_color.sub!(/fg-(\w{2,}+)/, 'fg-l-\1') - end - css_classes << fg_color - end - css_classes << @bg_color unless @bg_color.nil? - - STYLE_SWITCHES.each do |css_class, flag| - css_classes << "term-#{css_class}" if @style_mask & flag != 0 - end - - return if css_classes.empty? - - @out << %{<span class="#{css_classes.join(' ')}">} - @n_open_tags += 1 - end - - def close_open_tags - while @n_open_tags > 0 - @out << %{</span>} - @n_open_tags -= 1 - end - end - - def reset_state - @offset = 0 - @n_open_tags = 0 - @out = '' - reset - end - - def state - state = STATE_PARAMS.inject({}) do |h, param| - h[param] = send(param) # rubocop:disable GitlabSecurity/PublicSend - h - end - Base64.urlsafe_encode64(state.to_json) - end - - def restore_state(new_state, stream) - state = Base64.urlsafe_decode64(new_state) - state = JSON.parse(state, symbolize_names: true) - return if state[:offset].to_i > stream.size - - STATE_PARAMS.each do |param| - send("#{param}=".to_sym, state[param]) # rubocop:disable GitlabSecurity/PublicSend - end - end - - def reset - @fg_color = nil - @bg_color = nil - @style_mask = 0 - end - - def enable(flag) - @style_mask |= flag - end - - def disable(flag) - @style_mask &= ~flag - end - - def set_fg_color(color_index, prefix = nil) - @fg_color = get_term_color_class(color_index, ["fg", prefix]) - end - - def set_bg_color(color_index, prefix = nil) - @bg_color = get_term_color_class(color_index, ["bg", prefix]) - end - - def get_term_color_class(color_index, prefix) - color_name = COLOR[color_index] - return nil if color_name.nil? - - get_color_class(["term", prefix, color_name]) - end - - def set_fg_color_256(command_stack) - css_class = get_xterm_color_class(command_stack, "fg") - @fg_color = css_class unless css_class.nil? - end - - def set_bg_color_256(command_stack) - css_class = get_xterm_color_class(command_stack, "bg") - @bg_color = css_class unless css_class.nil? - end - - def get_xterm_color_class(command_stack, prefix) - # the 38 and 48 commands have to be followed by "5" and the color index - return unless command_stack.length >= 2 - return unless command_stack[0] == "5" - - command_stack.shift() # ignore the "5" command - color_index = command_stack.shift().to_i - - return unless color_index >= 0 - return unless color_index <= 255 - - get_color_class(["xterm", prefix, color_index]) - end - - def get_color_class(segments) - [segments].flatten.compact.join('-') - end - end - end -end diff --git a/lib/ci/assets/.gitkeep b/lib/ci/assets/.gitkeep deleted file mode 100644 index e69de29bb2d..00000000000 --- a/lib/ci/assets/.gitkeep +++ /dev/null diff --git a/lib/ci/charts.rb b/lib/ci/charts.rb deleted file mode 100644 index 76a69bf8a83..00000000000 --- a/lib/ci/charts.rb +++ /dev/null @@ -1,116 +0,0 @@ -module Ci - module Charts - module DailyInterval - def grouped_count(query) - query - .group("DATE(#{Ci::Pipeline.table_name}.created_at)") - .count(:created_at) - .transform_keys { |date| date.strftime(@format) } - end - - def interval_step - @interval_step ||= 1.day - end - end - - module MonthlyInterval - def grouped_count(query) - if Gitlab::Database.postgresql? - query - .group("to_char(#{Ci::Pipeline.table_name}.created_at, '01 Month YYYY')") - .count(:created_at) - .transform_keys(&:squish) - else - query - .group("DATE_FORMAT(#{Ci::Pipeline.table_name}.created_at, '01 %M %Y')") - .count(:created_at) - end - end - - def interval_step - @interval_step ||= 1.month - end - end - - class Chart - attr_reader :labels, :total, :success, :project, :pipeline_times - - def initialize(project) - @labels = [] - @total = [] - @success = [] - @pipeline_times = [] - @project = project - - collect - end - - def collect - query = project.pipelines - .where("? > #{Ci::Pipeline.table_name}.created_at AND #{Ci::Pipeline.table_name}.created_at > ?", @to, @from) # rubocop:disable GitlabSecurity/SqlInjection - - totals_count = grouped_count(query) - success_count = grouped_count(query.success) - - current = @from - while current < @to - label = current.strftime(@format) - - @labels << label - @total << (totals_count[label] || 0) - @success << (success_count[label] || 0) - - current += interval_step - end - end - end - - class YearChart < Chart - include MonthlyInterval - - def initialize(*) - @to = Date.today.end_of_month - @from = @to.years_ago(1).beginning_of_month - @format = '%d %B %Y' - - super - end - end - - class MonthChart < Chart - include DailyInterval - - def initialize(*) - @to = Date.today - @from = @to - 30.days - @format = '%d %B' - - super - end - end - - class WeekChart < Chart - include DailyInterval - - def initialize(*) - @to = Date.today - @from = @to - 7.days - @format = '%d %B' - - super - end - end - - class PipelineTime < Chart - def collect - commits = project.pipelines.last(30) - - commits.each do |commit| - @labels << commit.short_sha - duration = commit.duration || 0 - @pipeline_times << (duration / 60) - end - end - end - end -end diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb deleted file mode 100644 index 62b44389b15..00000000000 --- a/lib/ci/gitlab_ci_yaml_processor.rb +++ /dev/null @@ -1,251 +0,0 @@ -module Ci - class GitlabCiYamlProcessor - ValidationError = Class.new(StandardError) - - include Gitlab::Ci::Config::Entry::LegacyValidationHelpers - - attr_reader :path, :cache, :stages, :jobs - - def initialize(config, path = nil) - @ci_config = Gitlab::Ci::Config.new(config) - @config = @ci_config.to_hash - @path = path - - unless @ci_config.valid? - raise ValidationError, @ci_config.errors.first - end - - initial_parsing - rescue Gitlab::Ci::Config::Loader::FormatError => e - raise ValidationError, e.message - end - - def builds_for_stage_and_ref(stage, ref, tag = false, source = nil) - jobs_for_stage_and_ref(stage, ref, tag, source).map do |name, _| - build_attributes(name) - end - end - - def builds - @jobs.map do |name, _| - build_attributes(name) - end - end - - def stage_seeds(pipeline) - seeds = @stages.uniq.map do |stage| - builds = pipeline_stage_builds(stage, pipeline) - - Gitlab::Ci::Stage::Seed.new(pipeline, stage, builds) if builds.any? - end - - seeds.compact - end - - def build_attributes(name) - job = @jobs[name.to_sym] || {} - - { stage_idx: @stages.index(job[:stage]), - stage: job[:stage], - commands: job[:commands], - tag_list: job[:tags] || [], - name: job[:name].to_s, - allow_failure: job[:ignore], - when: job[:when] || 'on_success', - environment: job[:environment_name], - coverage_regex: job[:coverage], - yaml_variables: yaml_variables(name), - options: { - image: job[:image], - services: job[:services], - artifacts: job[:artifacts], - cache: job[:cache], - dependencies: job[:dependencies], - before_script: job[:before_script], - script: job[:script], - after_script: job[:after_script], - environment: job[:environment], - retry: job[:retry] - }.compact } - end - - def self.validation_message(content) - return 'Please provide content of .gitlab-ci.yml' if content.blank? - - begin - Ci::GitlabCiYamlProcessor.new(content) - nil - rescue ValidationError, Psych::SyntaxError => e - e.message - end - end - - private - - def pipeline_stage_builds(stage, pipeline) - builds = builds_for_stage_and_ref( - stage, pipeline.ref, pipeline.tag?, pipeline.source) - - builds.select do |build| - job = @jobs[build.fetch(:name).to_sym] - has_kubernetes = pipeline.has_kubernetes_active? - only_kubernetes = job.dig(:only, :kubernetes) - except_kubernetes = job.dig(:except, :kubernetes) - - [!only_kubernetes && !except_kubernetes, - only_kubernetes && has_kubernetes, - except_kubernetes && !has_kubernetes].any? - end - end - - def jobs_for_ref(ref, tag = false, source = nil) - @jobs.select do |_, job| - process?(job.dig(:only, :refs), job.dig(:except, :refs), ref, tag, source) - end - end - - def jobs_for_stage_and_ref(stage, ref, tag = false, source = nil) - jobs_for_ref(ref, tag, source).select do |_, job| - job[:stage] == stage - end - end - - def initial_parsing - ## - # Global config - # - @before_script = @ci_config.before_script - @image = @ci_config.image - @after_script = @ci_config.after_script - @services = @ci_config.services - @variables = @ci_config.variables - @stages = @ci_config.stages - @cache = @ci_config.cache - - ## - # Jobs - # - @jobs = @ci_config.jobs - - @jobs.each do |name, job| - # logical validation for job - - validate_job_stage!(name, job) - validate_job_dependencies!(name, job) - validate_job_environment!(name, job) - end - end - - def yaml_variables(name) - variables = (@variables || {}) - .merge(job_variables(name)) - - variables.map do |key, value| - { key: key.to_s, value: value, public: true } - end - end - - def job_variables(name) - job = @jobs[name.to_sym] - return {} unless job - - job[:variables] || {} - end - - def validate_job_stage!(name, job) - return unless job[:stage] - - unless job[:stage].is_a?(String) && job[:stage].in?(@stages) - raise ValidationError, "#{name} job: stage parameter should be #{@stages.join(", ")}" - end - end - - def validate_job_dependencies!(name, job) - return unless job[:dependencies] - - stage_index = @stages.index(job[:stage]) - - job[:dependencies].each do |dependency| - raise ValidationError, "#{name} job: undefined dependency: #{dependency}" unless @jobs[dependency.to_sym] - - unless @stages.index(@jobs[dependency.to_sym][:stage]) < stage_index - raise ValidationError, "#{name} job: dependency #{dependency} is not defined in prior stages" - end - end - end - - def validate_job_environment!(name, job) - return unless job[:environment] - return unless job[:environment].is_a?(Hash) - - environment = job[:environment] - validate_on_stop_job!(name, environment, environment[:on_stop]) - end - - def validate_on_stop_job!(name, environment, on_stop) - return unless on_stop - - on_stop_job = @jobs[on_stop.to_sym] - unless on_stop_job - raise ValidationError, "#{name} job: on_stop job #{on_stop} is not defined" - end - - unless on_stop_job[:environment] - raise ValidationError, "#{name} job: on_stop job #{on_stop} does not have environment defined" - end - - unless on_stop_job[:environment][:name] == environment[:name] - raise ValidationError, "#{name} job: on_stop job #{on_stop} have different environment name" - end - - unless on_stop_job[:environment][:action] == 'stop' - raise ValidationError, "#{name} job: on_stop job #{on_stop} needs to have action stop defined" - end - end - - def process?(only_params, except_params, ref, tag, source) - if only_params.present? - return false unless matching?(only_params, ref, tag, source) - end - - if except_params.present? - return false if matching?(except_params, ref, tag, source) - end - - true - end - - def matching?(patterns, ref, tag, source) - patterns.any? do |pattern| - pattern, path = pattern.split('@', 2) - matches_path?(path) && matches_pattern?(pattern, ref, tag, source) - end - end - - def matches_path?(path) - return true unless path - - path == self.path - end - - def matches_pattern?(pattern, ref, tag, source) - return true if tag && pattern == 'tags' - return true if !tag && pattern == 'branches' - return true if source_to_pattern(source) == pattern - - if pattern.first == "/" && pattern.last == "/" - Regexp.new(pattern[1...-1]) =~ ref - else - pattern == ref - end - end - - def source_to_pattern(source) - if %w[api external web].include?(source) - source - else - source&.pluralize - end - end - end -end diff --git a/lib/ci/mask_secret.rb b/lib/ci/mask_secret.rb deleted file mode 100644 index 997377abc55..00000000000 --- a/lib/ci/mask_secret.rb +++ /dev/null @@ -1,10 +0,0 @@ -module Ci::MaskSecret - class << self - def mask!(value, token) - return value unless value.present? && token.present? - - value.gsub!(token, 'x' * token.length) - value - end - end -end diff --git a/lib/ci/model.rb b/lib/ci/model.rb deleted file mode 100644 index c42a0ad36db..00000000000 --- a/lib/ci/model.rb +++ /dev/null @@ -1,11 +0,0 @@ -module Ci - module Model - def table_name_prefix - "ci_" - end - - def model_name - @model_name ||= ActiveModel::Name.new(self, nil, self.name.split("::").last) - end - end -end diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb index 3fd81759d25..11ace83c15c 100644 --- a/lib/gitlab/auth.rb +++ b/lib/gitlab/auth.rb @@ -2,7 +2,7 @@ module Gitlab module Auth MissingPersonalTokenError = Class.new(StandardError) - REGISTRY_SCOPES = [:read_registry].freeze + REGISTRY_SCOPES = Gitlab.config.registry.enabled ? [:read_registry].freeze : [].freeze # Scopes used for GitLab API access API_SCOPES = [:api, :read_user].freeze diff --git a/lib/gitlab/ci/ansi2html.rb b/lib/gitlab/ci/ansi2html.rb new file mode 100644 index 00000000000..ad78ae244b2 --- /dev/null +++ b/lib/gitlab/ci/ansi2html.rb @@ -0,0 +1,333 @@ +# ANSI color library +# +# Implementation per http://en.wikipedia.org/wiki/ANSI_escape_code +module Gitlab + module Ci + module Ansi2html + # keys represent the trailing digit in color changing command (30-37, 40-47, 90-97. 100-107) + COLOR = { + 0 => 'black', # not that this is gray in the intense color table + 1 => 'red', + 2 => 'green', + 3 => 'yellow', + 4 => 'blue', + 5 => 'magenta', + 6 => 'cyan', + 7 => 'white', # not that this is gray in the dark (aka default) color table + }.freeze + + STYLE_SWITCHES = { + bold: 0x01, + italic: 0x02, + underline: 0x04, + conceal: 0x08, + cross: 0x10 + }.freeze + + def self.convert(ansi, state = nil) + Converter.new.convert(ansi, state) + end + + class Converter + def on_0(s) reset() end + + def on_1(s) enable(STYLE_SWITCHES[:bold]) end + + def on_3(s) enable(STYLE_SWITCHES[:italic]) end + + def on_4(s) enable(STYLE_SWITCHES[:underline]) end + + def on_8(s) enable(STYLE_SWITCHES[:conceal]) end + + def on_9(s) enable(STYLE_SWITCHES[:cross]) end + + def on_21(s) disable(STYLE_SWITCHES[:bold]) end + + def on_22(s) disable(STYLE_SWITCHES[:bold]) end + + def on_23(s) disable(STYLE_SWITCHES[:italic]) end + + def on_24(s) disable(STYLE_SWITCHES[:underline]) end + + def on_28(s) disable(STYLE_SWITCHES[:conceal]) end + + def on_29(s) disable(STYLE_SWITCHES[:cross]) end + + def on_30(s) set_fg_color(0) end + + def on_31(s) set_fg_color(1) end + + def on_32(s) set_fg_color(2) end + + def on_33(s) set_fg_color(3) end + + def on_34(s) set_fg_color(4) end + + def on_35(s) set_fg_color(5) end + + def on_36(s) set_fg_color(6) end + + def on_37(s) set_fg_color(7) end + + def on_38(s) set_fg_color_256(s) end + + def on_39(s) set_fg_color(9) end + + def on_40(s) set_bg_color(0) end + + def on_41(s) set_bg_color(1) end + + def on_42(s) set_bg_color(2) end + + def on_43(s) set_bg_color(3) end + + def on_44(s) set_bg_color(4) end + + def on_45(s) set_bg_color(5) end + + def on_46(s) set_bg_color(6) end + + def on_47(s) set_bg_color(7) end + + def on_48(s) set_bg_color_256(s) end + + def on_49(s) set_bg_color(9) end + + def on_90(s) set_fg_color(0, 'l') end + + def on_91(s) set_fg_color(1, 'l') end + + def on_92(s) set_fg_color(2, 'l') end + + def on_93(s) set_fg_color(3, 'l') end + + def on_94(s) set_fg_color(4, 'l') end + + def on_95(s) set_fg_color(5, 'l') end + + def on_96(s) set_fg_color(6, 'l') end + + def on_97(s) set_fg_color(7, 'l') end + + def on_99(s) set_fg_color(9, 'l') end + + def on_100(s) set_bg_color(0, 'l') end + + def on_101(s) set_bg_color(1, 'l') end + + def on_102(s) set_bg_color(2, 'l') end + + def on_103(s) set_bg_color(3, 'l') end + + def on_104(s) set_bg_color(4, 'l') end + + def on_105(s) set_bg_color(5, 'l') end + + def on_106(s) set_bg_color(6, 'l') end + + def on_107(s) set_bg_color(7, 'l') end + + def on_109(s) set_bg_color(9, 'l') end + + attr_accessor :offset, :n_open_tags, :fg_color, :bg_color, :style_mask + + STATE_PARAMS = [:offset, :n_open_tags, :fg_color, :bg_color, :style_mask].freeze + + def convert(stream, new_state) + reset_state + restore_state(new_state, stream) if new_state.present? + + append = false + truncated = false + + cur_offset = stream.tell + if cur_offset > @offset + @offset = cur_offset + truncated = true + else + stream.seek(@offset) + append = @offset > 0 + end + start_offset = @offset + + open_new_tag + + stream.each_line do |line| + s = StringScanner.new(line) + until s.eos? + if s.scan(/\e([@-_])(.*?)([@-~])/) + handle_sequence(s) + elsif s.scan(/\e(([@-_])(.*?)?)?$/) + break + elsif s.scan(/</) + @out << '<' + elsif s.scan(/\r?\n/) + @out << '<br>' + else + @out << s.scan(/./m) + end + @offset += s.matched_size + end + end + + close_open_tags() + + OpenStruct.new( + html: @out.force_encoding(Encoding.default_external), + state: state, + append: append, + truncated: truncated, + offset: start_offset, + size: stream.tell - start_offset, + total: stream.size + ) + end + + def handle_sequence(s) + indicator = s[1] + commands = s[2].split ';' + terminator = s[3] + + # We are only interested in color and text style changes - triggered by + # sequences starting with '\e[' and ending with 'm'. Any other control + # sequence gets stripped (including stuff like "delete last line") + return unless indicator == '[' && terminator == 'm' + + close_open_tags() + + if commands.empty?() + reset() + return + end + + evaluate_command_stack(commands) + + open_new_tag + end + + def evaluate_command_stack(stack) + return unless command = stack.shift() + + if self.respond_to?("on_#{command}", true) + self.__send__("on_#{command}", stack) # rubocop:disable GitlabSecurity/PublicSend + end + + evaluate_command_stack(stack) + end + + def open_new_tag + css_classes = [] + + unless @fg_color.nil? + fg_color = @fg_color + # Most terminals show bold colored text in the light color variant + # Let's mimic that here + if @style_mask & STYLE_SWITCHES[:bold] != 0 + fg_color.sub!(/fg-(\w{2,}+)/, 'fg-l-\1') + end + css_classes << fg_color + end + css_classes << @bg_color unless @bg_color.nil? + + STYLE_SWITCHES.each do |css_class, flag| + css_classes << "term-#{css_class}" if @style_mask & flag != 0 + end + + return if css_classes.empty? + + @out << %{<span class="#{css_classes.join(' ')}">} + @n_open_tags += 1 + end + + def close_open_tags + while @n_open_tags > 0 + @out << %{</span>} + @n_open_tags -= 1 + end + end + + def reset_state + @offset = 0 + @n_open_tags = 0 + @out = '' + reset + end + + def state + state = STATE_PARAMS.inject({}) do |h, param| + h[param] = send(param) # rubocop:disable GitlabSecurity/PublicSend + h + end + Base64.urlsafe_encode64(state.to_json) + end + + def restore_state(new_state, stream) + state = Base64.urlsafe_decode64(new_state) + state = JSON.parse(state, symbolize_names: true) + return if state[:offset].to_i > stream.size + + STATE_PARAMS.each do |param| + send("#{param}=".to_sym, state[param]) # rubocop:disable GitlabSecurity/PublicSend + end + end + + def reset + @fg_color = nil + @bg_color = nil + @style_mask = 0 + end + + def enable(flag) + @style_mask |= flag + end + + def disable(flag) + @style_mask &= ~flag + end + + def set_fg_color(color_index, prefix = nil) + @fg_color = get_term_color_class(color_index, ["fg", prefix]) + end + + def set_bg_color(color_index, prefix = nil) + @bg_color = get_term_color_class(color_index, ["bg", prefix]) + end + + def get_term_color_class(color_index, prefix) + color_name = COLOR[color_index] + return nil if color_name.nil? + + get_color_class(["term", prefix, color_name]) + end + + def set_fg_color_256(command_stack) + css_class = get_xterm_color_class(command_stack, "fg") + @fg_color = css_class unless css_class.nil? + end + + def set_bg_color_256(command_stack) + css_class = get_xterm_color_class(command_stack, "bg") + @bg_color = css_class unless css_class.nil? + end + + def get_xterm_color_class(command_stack, prefix) + # the 38 and 48 commands have to be followed by "5" and the color index + return unless command_stack.length >= 2 + return unless command_stack[0] == "5" + + command_stack.shift() # ignore the "5" command + color_index = command_stack.shift().to_i + + return unless color_index >= 0 + return unless color_index <= 255 + + get_color_class(["xterm", prefix, color_index]) + end + + def get_color_class(segments) + [segments].flatten.compact.join('-') + end + end + end + end +end diff --git a/lib/gitlab/ci/charts.rb b/lib/gitlab/ci/charts.rb new file mode 100644 index 00000000000..7df7b542d91 --- /dev/null +++ b/lib/gitlab/ci/charts.rb @@ -0,0 +1,118 @@ +module Gitlab + module Ci + module Charts + module DailyInterval + def grouped_count(query) + query + .group("DATE(#{::Ci::Pipeline.table_name}.created_at)") + .count(:created_at) + .transform_keys { |date| date.strftime(@format) } + end + + def interval_step + @interval_step ||= 1.day + end + end + + module MonthlyInterval + def grouped_count(query) + if Gitlab::Database.postgresql? + query + .group("to_char(#{::Ci::Pipeline.table_name}.created_at, '01 Month YYYY')") + .count(:created_at) + .transform_keys(&:squish) + else + query + .group("DATE_FORMAT(#{::Ci::Pipeline.table_name}.created_at, '01 %M %Y')") + .count(:created_at) + end + end + + def interval_step + @interval_step ||= 1.month + end + end + + class Chart + attr_reader :labels, :total, :success, :project, :pipeline_times + + def initialize(project) + @labels = [] + @total = [] + @success = [] + @pipeline_times = [] + @project = project + + collect + end + + def collect + query = project.pipelines + .where("? > #{::Ci::Pipeline.table_name}.created_at AND #{::Ci::Pipeline.table_name}.created_at > ?", @to, @from) # rubocop:disable GitlabSecurity/SqlInjection + + totals_count = grouped_count(query) + success_count = grouped_count(query.success) + + current = @from + while current < @to + label = current.strftime(@format) + + @labels << label + @total << (totals_count[label] || 0) + @success << (success_count[label] || 0) + + current += interval_step + end + end + end + + class YearChart < Chart + include MonthlyInterval + + def initialize(*) + @to = Date.today.end_of_month + @from = @to.years_ago(1).beginning_of_month + @format = '%d %B %Y' + + super + end + end + + class MonthChart < Chart + include DailyInterval + + def initialize(*) + @to = Date.today + @from = @to - 30.days + @format = '%d %B' + + super + end + end + + class WeekChart < Chart + include DailyInterval + + def initialize(*) + @to = Date.today + @from = @to - 7.days + @format = '%d %B' + + super + end + end + + class PipelineTime < Chart + def collect + commits = project.pipelines.last(30) + + commits.each do |commit| + @labels << commit.short_sha + duration = commit.duration || 0 + @pipeline_times << (duration / 60) + end + end + end + end + end +end diff --git a/lib/gitlab/ci/mask_secret.rb b/lib/gitlab/ci/mask_secret.rb new file mode 100644 index 00000000000..0daddaa638c --- /dev/null +++ b/lib/gitlab/ci/mask_secret.rb @@ -0,0 +1,12 @@ +module Gitlab + module Ci::MaskSecret + class << self + def mask!(value, token) + return value unless value.present? && token.present? + + value.gsub!(token, 'x' * token.length) + value + end + end + end +end diff --git a/lib/gitlab/ci/model.rb b/lib/gitlab/ci/model.rb new file mode 100644 index 00000000000..3994a50772b --- /dev/null +++ b/lib/gitlab/ci/model.rb @@ -0,0 +1,13 @@ +module Gitlab + module Ci + module Model + def table_name_prefix + "ci_" + end + + def model_name + @model_name ||= ActiveModel::Name.new(self, nil, self.name.split("::").last) + end + end + end +end diff --git a/lib/gitlab/ci/trace/stream.rb b/lib/gitlab/ci/trace/stream.rb index 8503ecf8700..ab3408f48d6 100644 --- a/lib/gitlab/ci/trace/stream.rb +++ b/lib/gitlab/ci/trace/stream.rb @@ -56,13 +56,13 @@ module Gitlab end def html_with_state(state = nil) - ::Ci::Ansi2html.convert(stream, state) + ::Gitlab::Ci::Ansi2html.convert(stream, state) end def html(last_lines: nil) text = raw(last_lines: last_lines) buffer = StringIO.new(text) - ::Ci::Ansi2html.convert(buffer).html + ::Gitlab::Ci::Ansi2html.convert(buffer).html end def extract_coverage(regex) diff --git a/lib/gitlab/ci/yaml_processor.rb b/lib/gitlab/ci/yaml_processor.rb new file mode 100644 index 00000000000..7582964b24e --- /dev/null +++ b/lib/gitlab/ci/yaml_processor.rb @@ -0,0 +1,253 @@ +module Gitlab + module Ci + class YamlProcessor + ValidationError = Class.new(StandardError) + + include Gitlab::Ci::Config::Entry::LegacyValidationHelpers + + attr_reader :path, :cache, :stages, :jobs + + def initialize(config, path = nil) + @ci_config = Gitlab::Ci::Config.new(config) + @config = @ci_config.to_hash + @path = path + + unless @ci_config.valid? + raise ValidationError, @ci_config.errors.first + end + + initial_parsing + rescue Gitlab::Ci::Config::Loader::FormatError => e + raise ValidationError, e.message + end + + def builds_for_stage_and_ref(stage, ref, tag = false, source = nil) + jobs_for_stage_and_ref(stage, ref, tag, source).map do |name, _| + build_attributes(name) + end + end + + def builds + @jobs.map do |name, _| + build_attributes(name) + end + end + + def stage_seeds(pipeline) + seeds = @stages.uniq.map do |stage| + builds = pipeline_stage_builds(stage, pipeline) + + Gitlab::Ci::Stage::Seed.new(pipeline, stage, builds) if builds.any? + end + + seeds.compact + end + + def build_attributes(name) + job = @jobs[name.to_sym] || {} + + { stage_idx: @stages.index(job[:stage]), + stage: job[:stage], + commands: job[:commands], + tag_list: job[:tags] || [], + name: job[:name].to_s, + allow_failure: job[:ignore], + when: job[:when] || 'on_success', + environment: job[:environment_name], + coverage_regex: job[:coverage], + yaml_variables: yaml_variables(name), + options: { + image: job[:image], + services: job[:services], + artifacts: job[:artifacts], + cache: job[:cache], + dependencies: job[:dependencies], + before_script: job[:before_script], + script: job[:script], + after_script: job[:after_script], + environment: job[:environment], + retry: job[:retry] + }.compact } + end + + def self.validation_message(content) + return 'Please provide content of .gitlab-ci.yml' if content.blank? + + begin + Gitlab::Ci::YamlProcessor.new(content) + nil + rescue ValidationError, Psych::SyntaxError => e + e.message + end + end + + private + + def pipeline_stage_builds(stage, pipeline) + builds = builds_for_stage_and_ref( + stage, pipeline.ref, pipeline.tag?, pipeline.source) + + builds.select do |build| + job = @jobs[build.fetch(:name).to_sym] + has_kubernetes = pipeline.has_kubernetes_active? + only_kubernetes = job.dig(:only, :kubernetes) + except_kubernetes = job.dig(:except, :kubernetes) + + [!only_kubernetes && !except_kubernetes, + only_kubernetes && has_kubernetes, + except_kubernetes && !has_kubernetes].any? + end + end + + def jobs_for_ref(ref, tag = false, source = nil) + @jobs.select do |_, job| + process?(job.dig(:only, :refs), job.dig(:except, :refs), ref, tag, source) + end + end + + def jobs_for_stage_and_ref(stage, ref, tag = false, source = nil) + jobs_for_ref(ref, tag, source).select do |_, job| + job[:stage] == stage + end + end + + def initial_parsing + ## + # Global config + # + @before_script = @ci_config.before_script + @image = @ci_config.image + @after_script = @ci_config.after_script + @services = @ci_config.services + @variables = @ci_config.variables + @stages = @ci_config.stages + @cache = @ci_config.cache + + ## + # Jobs + # + @jobs = @ci_config.jobs + + @jobs.each do |name, job| + # logical validation for job + + validate_job_stage!(name, job) + validate_job_dependencies!(name, job) + validate_job_environment!(name, job) + end + end + + def yaml_variables(name) + variables = (@variables || {}) + .merge(job_variables(name)) + + variables.map do |key, value| + { key: key.to_s, value: value, public: true } + end + end + + def job_variables(name) + job = @jobs[name.to_sym] + return {} unless job + + job[:variables] || {} + end + + def validate_job_stage!(name, job) + return unless job[:stage] + + unless job[:stage].is_a?(String) && job[:stage].in?(@stages) + raise ValidationError, "#{name} job: stage parameter should be #{@stages.join(", ")}" + end + end + + def validate_job_dependencies!(name, job) + return unless job[:dependencies] + + stage_index = @stages.index(job[:stage]) + + job[:dependencies].each do |dependency| + raise ValidationError, "#{name} job: undefined dependency: #{dependency}" unless @jobs[dependency.to_sym] + + unless @stages.index(@jobs[dependency.to_sym][:stage]) < stage_index + raise ValidationError, "#{name} job: dependency #{dependency} is not defined in prior stages" + end + end + end + + def validate_job_environment!(name, job) + return unless job[:environment] + return unless job[:environment].is_a?(Hash) + + environment = job[:environment] + validate_on_stop_job!(name, environment, environment[:on_stop]) + end + + def validate_on_stop_job!(name, environment, on_stop) + return unless on_stop + + on_stop_job = @jobs[on_stop.to_sym] + unless on_stop_job + raise ValidationError, "#{name} job: on_stop job #{on_stop} is not defined" + end + + unless on_stop_job[:environment] + raise ValidationError, "#{name} job: on_stop job #{on_stop} does not have environment defined" + end + + unless on_stop_job[:environment][:name] == environment[:name] + raise ValidationError, "#{name} job: on_stop job #{on_stop} have different environment name" + end + + unless on_stop_job[:environment][:action] == 'stop' + raise ValidationError, "#{name} job: on_stop job #{on_stop} needs to have action stop defined" + end + end + + def process?(only_params, except_params, ref, tag, source) + if only_params.present? + return false unless matching?(only_params, ref, tag, source) + end + + if except_params.present? + return false if matching?(except_params, ref, tag, source) + end + + true + end + + def matching?(patterns, ref, tag, source) + patterns.any? do |pattern| + pattern, path = pattern.split('@', 2) + matches_path?(path) && matches_pattern?(pattern, ref, tag, source) + end + end + + def matches_path?(path) + return true unless path + + path == self.path + end + + def matches_pattern?(pattern, ref, tag, source) + return true if tag && pattern == 'tags' + return true if !tag && pattern == 'branches' + return true if source_to_pattern(source) == pattern + + if pattern.first == "/" && pattern.last == "/" + Regexp.new(pattern[1...-1]) =~ ref + else + pattern == ref + end + end + + def source_to_pattern(source) + if %w[api external web].include?(source) + source + else + source&.pluralize + end + end + end + end +end diff --git a/lib/gitlab/conflict/parser.rb b/lib/gitlab/conflict/parser.rb index 84f9ecd3d23..e3678c914db 100644 --- a/lib/gitlab/conflict/parser.rb +++ b/lib/gitlab/conflict/parser.rb @@ -12,12 +12,7 @@ module Gitlab MissingEndDelimiter = Class.new(ParserError) def parse(text, our_path:, their_path:, parent_file: nil) - raise UnmergeableFile if text.blank? # Typically a binary file - raise UnmergeableFile if text.length > 200.kilobytes - - text.force_encoding('UTF-8') - - raise UnsupportedEncoding unless text.valid_encoding? + validate_text!(text) line_obj_index = 0 line_old = 1 @@ -32,15 +27,15 @@ module Gitlab full_line = line.delete("\n") if full_line == conflict_start - raise UnexpectedDelimiter unless type.nil? + validate_delimiter!(type.nil?) type = 'new' elsif full_line == conflict_middle - raise UnexpectedDelimiter unless type == 'new' + validate_delimiter!(type == 'new') type = 'old' elsif full_line == conflict_end - raise UnexpectedDelimiter unless type == 'old' + validate_delimiter!(type == 'old') type = nil elsif line[0] == '\\' @@ -59,6 +54,21 @@ module Gitlab lines end + + private + + def validate_text!(text) + raise UnmergeableFile if text.blank? # Typically a binary file + raise UnmergeableFile if text.length > 200.kilobytes + + text.force_encoding('UTF-8') + + raise UnsupportedEncoding unless text.valid_encoding? + end + + def validate_delimiter!(condition) + raise UnexpectedDelimiter unless condition + end end end end diff --git a/lib/gitlab/database/read_only_relation.rb b/lib/gitlab/database/read_only_relation.rb new file mode 100644 index 00000000000..4571ad122ce --- /dev/null +++ b/lib/gitlab/database/read_only_relation.rb @@ -0,0 +1,16 @@ +module Gitlab + module Database + # Module that can be injected into a ActiveRecord::Relation to make it + # read-only. + module ReadOnlyRelation + [:delete, :delete_all, :update, :update_all].each do |method| + define_method(method) do |*args| + raise( + ActiveRecord::ReadOnlyRecord, + "This relation is marked as read-only" + ) + end + end + end + end +end diff --git a/lib/gitlab/diff/inline_diff_marker.rb b/lib/gitlab/diff/inline_diff_marker.rb index 919965100ae..010b4be7b40 100644 --- a/lib/gitlab/diff/inline_diff_marker.rb +++ b/lib/gitlab/diff/inline_diff_marker.rb @@ -2,9 +2,10 @@ module Gitlab module Diff class InlineDiffMarker < Gitlab::StringRangeMarker def mark(line_inline_diffs, mode: nil) - super(line_inline_diffs) do |text, left:, right:| + mark = super(line_inline_diffs) do |text, left:, right:| %{<span class="#{html_class_names(left, right, mode)}">#{text}</span>} end + mark.html_safe end private diff --git a/lib/gitlab/git.rb b/lib/gitlab/git.rb index 8c9acbc9fbe..b4b6326cfdd 100644 --- a/lib/gitlab/git.rb +++ b/lib/gitlab/git.rb @@ -11,7 +11,7 @@ module Gitlab include Gitlab::EncodingHelper def ref_name(ref) - encode! ref.sub(/\Arefs\/(tags|heads|remotes)\//, '') + encode_utf8(ref).sub(/\Arefs\/(tags|heads|remotes)\//, '') end def branch_name(ref) diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb index 5ee6669050c..1f370686186 100644 --- a/lib/gitlab/git/commit.rb +++ b/lib/gitlab/git/commit.rb @@ -352,7 +352,7 @@ module Gitlab end def stats - Gitlab::Git::CommitStats.new(self) + Gitlab::Git::CommitStats.new(@repository, self) end def to_patch(options = {}) diff --git a/lib/gitlab/git/commit_stats.rb b/lib/gitlab/git/commit_stats.rb index 00acb4763e9..6bf49a0af18 100644 --- a/lib/gitlab/git/commit_stats.rb +++ b/lib/gitlab/git/commit_stats.rb @@ -10,12 +10,29 @@ module Gitlab # Instantiate a CommitStats object # # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/323 - def initialize(commit) + def initialize(repo, commit) @id = commit.id @additions = 0 @deletions = 0 @total = 0 + repo.gitaly_migrate(:commit_stats) do |is_enabled| + if is_enabled + gitaly_stats(repo, commit) + else + rugged_stats(commit) + end + end + end + + def gitaly_stats(repo, commit) + stats = repo.gitaly_commit_client.commit_stats(@id) + @additions = stats.additions + @deletions = stats.deletions + @total = @additions + @deletions + end + + def rugged_stats(commit) diff = commit.rugged_diff_from_parent diff.each_patch do |p| diff --git a/lib/gitlab/git/operation_service.rb b/lib/gitlab/git/operation_service.rb index 9e6fca8c80c..347e3b5165e 100644 --- a/lib/gitlab/git/operation_service.rb +++ b/lib/gitlab/git/operation_service.rb @@ -1,11 +1,13 @@ module Gitlab module Git class OperationService - attr_reader :committer, :repository + attr_reader :user, :repository - def initialize(committer, new_repository) - committer = Gitlab::Git::Committer.from_user(committer) if committer.is_a?(User) - @committer = committer + def initialize(user, new_repository) + if user + user = Gitlab::Git::User.from_gitlab(user) unless user.respond_to?(:gl_id) + @user = user + end # Refactoring aid unless new_repository.is_a?(Gitlab::Git::Repository) @@ -128,7 +130,7 @@ module Gitlab def with_hooks(ref, newrev, oldrev) Gitlab::Git::HooksService.new.execute( - committer, + user, repository, oldrev, newrev, diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index efa13590a2c..32a265b15f2 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -610,43 +610,43 @@ module Gitlab # TODO: implement this method end - def add_branch(branch_name, committer:, target:) + def add_branch(branch_name, user:, target:) target_object = Ref.dereference_object(lookup(target)) raise InvalidRef.new("target not found: #{target}") unless target_object - OperationService.new(committer, self).add_branch(branch_name, target_object.oid) + OperationService.new(user, self).add_branch(branch_name, target_object.oid) find_branch(branch_name) rescue Rugged::ReferenceError => ex raise InvalidRef, ex end - def add_tag(tag_name, committer:, target:, message: nil) + def add_tag(tag_name, user:, target:, message: nil) target_object = Ref.dereference_object(lookup(target)) raise InvalidRef.new("target not found: #{target}") unless target_object - committer = Committer.from_user(committer) if committer.is_a?(User) + user = Gitlab::Git::User.from_gitlab(user) unless user.respond_to?(:gl_id) options = nil # Use nil, not the empty hash. Rugged cares about this. if message options = { message: message, - tagger: Gitlab::Git.committer_hash(email: committer.email, name: committer.name) + tagger: Gitlab::Git.committer_hash(email: user.email, name: user.name) } end - OperationService.new(committer, self).add_tag(tag_name, target_object.oid, options) + OperationService.new(user, self).add_tag(tag_name, target_object.oid, options) find_tag(tag_name) rescue Rugged::ReferenceError => ex raise InvalidRef, ex end - def rm_branch(branch_name, committer:) - OperationService.new(committer, self).rm_branch(find_branch(branch_name)) + def rm_branch(branch_name, user:) + OperationService.new(user, self).rm_branch(find_branch(branch_name)) end - def rm_tag(tag_name, committer:) - OperationService.new(committer, self).rm_tag(find_tag(tag_name)) + def rm_tag(tag_name, user:) + OperationService.new(user, self).rm_tag(find_tag(tag_name)) end def find_tag(name) diff --git a/lib/gitlab/git/committer.rb b/lib/gitlab/git/user.rb index 1f4bcf7a3a0..ea634d39668 100644 --- a/lib/gitlab/git/committer.rb +++ b/lib/gitlab/git/user.rb @@ -1,10 +1,14 @@ module Gitlab module Git - class Committer + class User attr_reader :name, :email, :gl_id - def self.from_user(user) - new(user.name, user.email, Gitlab::GlId.gl_id(user)) + def self.from_gitlab(gitlab_user) + new(gitlab_user.name, gitlab_user.email, Gitlab::GlId.gl_id(gitlab_user)) + end + + def self.from_gitaly(gitaly_user) + new(gitaly_user.name, gitaly_user.email, gitaly_user.gl_id) end def initialize(name, email, gl_id) diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb index 0825a3a7694..1ba1a7830a4 100644 --- a/lib/gitlab/gitaly_client/commit_service.rb +++ b/lib/gitlab/gitaly_client/commit_service.rb @@ -204,6 +204,14 @@ module Gitlab response.sum(&:data) end + def commit_stats(revision) + request = Gitaly::CommitStatsRequest.new( + repository: @gitaly_repo, + revision: GitalyClient.encode(revision) + ) + GitalyClient.call(@repository.storage, :commit_service, :commit_stats, request) + end + private def commit_diff_request_params(commit, options = {}) diff --git a/lib/gitlab/gpg.rb b/lib/gitlab/gpg.rb index 025f826e65f..0d5039ddf5f 100644 --- a/lib/gitlab/gpg.rb +++ b/lib/gitlab/gpg.rb @@ -69,11 +69,17 @@ module Gitlab def optimistic_using_tmp_keychain previous_dir = current_home_dir - Dir.mktmpdir do |dir| - GPGME::Engine.home_dir = dir - yield - end + tmp_dir = Dir.mktmpdir + GPGME::Engine.home_dir = tmp_dir + yield ensure + # Ignore any errors when removing the tmp directory, as we may run into a + # race condition: + # The `gpg-agent` agent process may clean up some files as well while + # `FileUtils.remove_entry` is iterating the directory and removing all + # its contained files and directories recursively, which could raise an + # error. + FileUtils.remove_entry(tmp_dir, true) GPGME::Engine.home_dir = previous_dir end end diff --git a/lib/gitlab/group_hierarchy.rb b/lib/gitlab/group_hierarchy.rb index 5a31e56cb30..635f52131f9 100644 --- a/lib/gitlab/group_hierarchy.rb +++ b/lib/gitlab/group_hierarchy.rb @@ -22,7 +22,7 @@ module Gitlab def base_and_ancestors return ancestors_base unless Group.supports_nested_groups? - base_and_ancestors_cte.apply_to(model.all) + read_only(base_and_ancestors_cte.apply_to(model.all)) end # Returns a relation that includes the descendants_base set of groups @@ -30,7 +30,7 @@ module Gitlab def base_and_descendants return descendants_base unless Group.supports_nested_groups? - base_and_descendants_cte.apply_to(model.all) + read_only(base_and_descendants_cte.apply_to(model.all)) end # Returns a relation that includes the base groups, their ancestors, @@ -67,11 +67,13 @@ module Gitlab union = SQL::Union.new([model.unscoped.from(ancestors_table), model.unscoped.from(descendants_table)]) - model + relation = model .unscoped .with .recursive(ancestors.to_arel, descendants.to_arel) .from("(#{union.to_sql}) #{model.table_name}") + + read_only(relation) end private @@ -107,5 +109,12 @@ module Gitlab def groups_table model.arel_table end + + def read_only(relation) + # relations using a CTE are not safe to use with update_all as it will + # throw away the CTE, hence we mark them as read-only. + relation.extend(Gitlab::Database::ReadOnlyRelation) + relation + end end end diff --git a/lib/gitlab/mail_room.rb b/lib/gitlab/mail_room.rb index 9f432673a6e..344784c866f 100644 --- a/lib/gitlab/mail_room.rb +++ b/lib/gitlab/mail_room.rb @@ -4,6 +4,15 @@ require_relative 'redis/queues' unless defined?(Gitlab::Redis::Queues) module Gitlab module MailRoom + DEFAULT_CONFIG = { + enabled: false, + port: 143, + ssl: false, + start_tls: false, + mailbox: 'inbox', + idle_timeout: 60 + }.freeze + class << self def enabled? config[:enabled] && config[:address] @@ -22,16 +31,10 @@ module Gitlab def fetch_config return {} unless File.exist?(config_file) - rails_env = ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development' - all_config = YAML.load_file(config_file)[rails_env].deep_symbolize_keys - - config = all_config[:incoming_email] || {} - config[:enabled] = false if config[:enabled].nil? - config[:port] = 143 if config[:port].nil? - config[:ssl] = false if config[:ssl].nil? - config[:start_tls] = false if config[:start_tls].nil? - config[:mailbox] = 'inbox' if config[:mailbox].nil? - config[:idle_timeout] = 60 if config[:idle_timeout].nil? + config = YAML.load_file(config_file)[rails_env].deep_symbolize_keys[:incoming_email] || {} + config = DEFAULT_CONFIG.merge(config) do |_key, oldval, newval| + newval.nil? ? oldval : newval + end if config[:enabled] && config[:address] gitlab_redis_queues = Gitlab::Redis::Queues.new(rails_env) @@ -45,6 +48,10 @@ module Gitlab config end + def rails_env + @rails_env ||= ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development' + end + def config_file ENV['MAIL_ROOM_GITLAB_CONFIG_FILE'] || File.expand_path('../../../config/gitlab.yml', __FILE__) end diff --git a/lib/gitlab/o_auth/auth_hash.rb b/lib/gitlab/o_auth/auth_hash.rb index 1f331b1e91d..5b5ed449f94 100644 --- a/lib/gitlab/o_auth/auth_hash.rb +++ b/lib/gitlab/o_auth/auth_hash.rb @@ -13,7 +13,7 @@ module Gitlab end def provider - @provider ||= Gitlab::Utils.force_utf8(auth_hash.provider.to_s) + @provider ||= auth_hash.provider.to_s end def name diff --git a/lib/gitlab/pages.rb b/lib/gitlab/pages.rb new file mode 100644 index 00000000000..981ef8faa9a --- /dev/null +++ b/lib/gitlab/pages.rb @@ -0,0 +1,5 @@ +module Gitlab + module Pages + VERSION = File.read(Rails.root.join("GITLAB_PAGES_VERSION")).strip.freeze + end +end diff --git a/lib/gitlab/themes.rb b/lib/gitlab/themes.rb new file mode 100644 index 00000000000..d43eff5ba4a --- /dev/null +++ b/lib/gitlab/themes.rb @@ -0,0 +1,84 @@ +module Gitlab + # Module containing GitLab's application theme definitions and helper methods + # for accessing them. + module Themes + extend self + + # Theme ID used when no `default_theme` configuration setting is provided. + APPLICATION_DEFAULT = 1 + + # Struct class representing a single Theme + Theme = Struct.new(:id, :name, :css_class) + + # All available Themes + THEMES = [ + Theme.new(1, 'Indigo', 'ui_indigo'), + Theme.new(2, 'Dark', 'ui_dark'), + Theme.new(3, 'Light', 'ui_light'), + Theme.new(4, 'Blue', 'ui_blue'), + Theme.new(5, 'Green', 'ui_green') + ].freeze + + # Convenience method to get a space-separated String of all the theme + # classes that might be applied to the `body` element + # + # Returns a String + def body_classes + THEMES.collect(&:css_class).uniq.join(' ') + end + + # Get a Theme by its ID + # + # If the ID is invalid, returns the default Theme. + # + # id - Integer ID + # + # Returns a Theme + def by_id(id) + THEMES.detect { |t| t.id == id } || default + end + + # Returns the number of defined Themes + def count + THEMES.size + end + + # Get the default Theme + # + # Returns a Theme + def default + by_id(default_id) + end + + # Iterate through each Theme + # + # Yields the Theme object + def each(&block) + THEMES.each(&block) + end + + # Get the Theme for the specified user, or the default + # + # user - User record + # + # Returns a Theme + def for_user(user) + if user + by_id(user.theme_id) + else + default + end + end + + private + + def default_id + @default_id ||= begin + id = Gitlab.config.gitlab.default_theme.to_i + theme_ids = THEMES.map(&:id) + + theme_ids.include?(id) ? id : APPLICATION_DEFAULT + end + end + end +end diff --git a/lib/gitlab/url_sanitizer.rb b/lib/gitlab/url_sanitizer.rb index 703adae12cb..4e1ec1402ea 100644 --- a/lib/gitlab/url_sanitizer.rb +++ b/lib/gitlab/url_sanitizer.rb @@ -19,13 +19,12 @@ module Gitlab end def initialize(url, credentials: nil) - @url = Addressable::URI.parse(url.to_s.strip) - %i[user password].each do |symbol| credentials[symbol] = credentials[symbol].presence if credentials&.key?(symbol) end @credentials = credentials + @url = parse_url(url) end def sanitized_url @@ -49,12 +48,30 @@ module Gitlab private + def parse_url(url) + url = url.to_s.strip + match = url.match(%r{\A(?:git|ssh|http(?:s?))\://(?:(.+)(?:@))?(.+)}) + raw_credentials = match[1] if match + + if raw_credentials.present? + url.sub!("#{raw_credentials}@", '') + + user, password = raw_credentials.split(':') + @credentials ||= { user: user.presence, password: password.presence } + end + + url = Addressable::URI.parse(url) + url.password = password if password.present? + url.user = user if user.present? + url + end + def generate_full_url return @url unless valid_credentials? @full_url = @url.dup - @full_url.password = credentials[:password] - @full_url.user = credentials[:user] + @full_url.password = credentials[:password] if credentials[:password].present? + @full_url.user = credentials[:user] if credentials[:user].present? @full_url end diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb index 3cf26625108..36708078136 100644 --- a/lib/gitlab/usage_data.rb +++ b/lib/gitlab/usage_data.rb @@ -22,9 +22,13 @@ module Gitlab ci_builds: ::Ci::Build.count, ci_internal_pipelines: ::Ci::Pipeline.internal.count, ci_external_pipelines: ::Ci::Pipeline.external.count, + ci_pipeline_config_auto_devops: ::Ci::Pipeline.auto_devops_source.count, + ci_pipeline_config_repository: ::Ci::Pipeline.repository_source.count, ci_runners: ::Ci::Runner.count, ci_triggers: ::Ci::Trigger.count, ci_pipeline_schedules: ::Ci::PipelineSchedule.count, + auto_devops_enabled: ::ProjectAutoDevops.enabled.count, + auto_devops_disabled: ::ProjectAutoDevops.disabled.count, deploy_keys: DeployKey.count, deployments: Deployment.count, environments: ::Environment.count, diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb index 7a94af2f8f1..17550cf9074 100644 --- a/lib/gitlab/workhorse.rb +++ b/lib/gitlab/workhorse.rb @@ -121,10 +121,10 @@ module Gitlab ] end - def send_artifacts_entry(build, path) + def send_artifacts_entry(build, entry) params = { 'Archive' => build.artifacts_file.path, - 'Entry' => Base64.encode64(path.to_s) + 'Entry' => Base64.encode64(entry.to_s) } [ diff --git a/lib/system_check/orphans/namespace_check.rb b/lib/system_check/orphans/namespace_check.rb new file mode 100644 index 00000000000..b8446300f72 --- /dev/null +++ b/lib/system_check/orphans/namespace_check.rb @@ -0,0 +1,54 @@ +module SystemCheck + module Orphans + class NamespaceCheck < SystemCheck::BaseCheck + set_name 'Orphaned namespaces:' + + def multi_check + Gitlab.config.repositories.storages.each do |storage_name, repository_storage| + $stdout.puts + $stdout.puts "* Storage: #{storage_name} (#{repository_storage['path']})".color(:yellow) + toplevel_namespace_dirs = disk_namespaces(repository_storage['path']) + + orphans = (toplevel_namespace_dirs - existing_namespaces) + print_orphans(orphans, storage_name) + end + + clear_namespaces! # releases memory when check finishes + end + + private + + def print_orphans(orphans, storage_name) + if orphans.empty? + $stdout.puts "* No orphaned namespaces for #{storage_name} storage".color(:green) + return + end + + orphans.each do |orphan| + $stdout.puts " - #{orphan}".color(:red) + end + end + + def disk_namespaces(storage_path) + fetch_disk_namespaces(storage_path).each_with_object([]) do |namespace_path, result| + namespace = File.basename(namespace_path) + next if namespace.eql?('@hashed') + + result << namespace + end + end + + def fetch_disk_namespaces(storage_path) + Dir.glob(File.join(storage_path, '*')) + end + + def existing_namespaces + @namespaces ||= Namespace.where(parent: nil).all.pluck(:path) + end + + def clear_namespaces! + @namespaces = nil + end + end + end +end diff --git a/lib/system_check/orphans/repository_check.rb b/lib/system_check/orphans/repository_check.rb new file mode 100644 index 00000000000..9b6b2429783 --- /dev/null +++ b/lib/system_check/orphans/repository_check.rb @@ -0,0 +1,68 @@ +module SystemCheck + module Orphans + class RepositoryCheck < SystemCheck::BaseCheck + set_name 'Orphaned repositories:' + attr_accessor :orphans + + def multi_check + Gitlab.config.repositories.storages.each do |storage_name, repository_storage| + $stdout.puts + $stdout.puts "* Storage: #{storage_name} (#{repository_storage['path']})".color(:yellow) + + repositories = disk_repositories(repository_storage['path']) + orphans = (repositories - fetch_repositories(storage_name)) + + print_orphans(orphans, storage_name) + end + end + + private + + def print_orphans(orphans, storage_name) + if orphans.empty? + $stdout.puts "* No orphaned repositories for #{storage_name} storage".color(:green) + return + end + + orphans.each do |orphan| + $stdout.puts " - #{orphan}".color(:red) + end + end + + def disk_repositories(storage_path) + fetch_disk_namespaces(storage_path).each_with_object([]) do |namespace_path, result| + namespace = File.basename(namespace_path) + next if namespace.eql?('@hashed') + + fetch_disk_repositories(namespace_path).each do |repo| + result << "#{namespace}/#{File.basename(repo)}" + end + end + end + + def fetch_repositories(storage_name) + sql = " + SELECT + CONCAT(n.path, '/', p.path, '.git') repo, + CONCAT(n.path, '/', p.path, '.wiki.git') wiki + FROM projects p + JOIN namespaces n + ON (p.namespace_id = n.id AND + n.parent_id IS NULL) + WHERE (p.repository_storage LIKE ?) + " + + query = ActiveRecord::Base.send(:sanitize_sql_array, [sql, storage_name]) # rubocop:disable GitlabSecurity/PublicSend + ActiveRecord::Base.connection.select_all(query).rows.try(:flatten!) || [] + end + + def fetch_disk_namespaces(storage_path) + Dir.glob(File.join(storage_path, '*')) + end + + def fetch_disk_repositories(namespace_path) + Dir.glob(File.join(namespace_path, '*')) + end + end + end +end diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake index 654f638c454..dfade1f3885 100644 --- a/lib/tasks/gitlab/check.rake +++ b/lib/tasks/gitlab/check.rake @@ -398,6 +398,35 @@ namespace :gitlab do end end + namespace :orphans do + desc 'Gitlab | Check for orphaned namespaces and repositories' + task check: :environment do + warn_user_is_not_gitlab + checks = [ + SystemCheck::Orphans::NamespaceCheck, + SystemCheck::Orphans::RepositoryCheck + ] + + SystemCheck.run('Orphans', checks) + end + + desc 'GitLab | Check for orphaned namespaces in the repositories path' + task check_namespaces: :environment do + warn_user_is_not_gitlab + checks = [SystemCheck::Orphans::NamespaceCheck] + + SystemCheck.run('Orphans', checks) + end + + desc 'GitLab | Check for orphaned repositories in the repositories path' + task check_repositories: :environment do + warn_user_is_not_gitlab + checks = [SystemCheck::Orphans::RepositoryCheck] + + SystemCheck.run('Orphans', checks) + end + end + namespace :user do desc "GitLab | Check the integrity of a specific user's repositories" task :check_repos, [:username] => :environment do |t, args| diff --git a/spec/controllers/boards/issues_controller_spec.rb b/spec/controllers/boards/issues_controller_spec.rb index dfa06c78d46..5163099cd98 100644 --- a/spec/controllers/boards/issues_controller_spec.rb +++ b/spec/controllers/boards/issues_controller_spec.rb @@ -45,6 +45,17 @@ describe Boards::IssuesController do expect(parsed_response.length).to eq 2 expect(development.issues.map(&:relative_position)).not_to include(nil) end + + it 'avoids N+1 database queries' do + create(:labeled_issue, project: project, labels: [development]) + control_count = ActiveRecord::QueryRecorder.new { list_issues(user: user, board: board, list: list2) }.count + + # 25 issues is bigger than the page size + # the relative position will ignore the `#make_sure_position_set` queries + create_list(:labeled_issue, 25, project: project, labels: [development], assignees: [johndoe], relative_position: 1) + + expect { list_issues(user: user, board: board, list: list2) }.not_to exceed_query_limit(control_count) + end end context 'with invalid list id' do diff --git a/spec/controllers/profiles/preferences_controller_spec.rb b/spec/controllers/profiles/preferences_controller_spec.rb index a5f544b4f92..a66b4ab0902 100644 --- a/spec/controllers/profiles/preferences_controller_spec.rb +++ b/spec/controllers/profiles/preferences_controller_spec.rb @@ -25,7 +25,8 @@ describe Profiles::PreferencesController do def go(params: {}, format: :js) params.reverse_merge!( color_scheme_id: '1', - dashboard: 'stars' + dashboard: 'stars', + theme_id: '1' ) patch :update, user: params, format: format @@ -40,7 +41,8 @@ describe Profiles::PreferencesController do it "changes the user's preferences" do prefs = { color_scheme_id: '1', - dashboard: 'stars' + dashboard: 'stars', + theme_id: '2' }.with_indifferent_access expect(user).to receive(:assign_attributes).with(prefs) diff --git a/spec/controllers/projects/branches_controller_spec.rb b/spec/controllers/projects/branches_controller_spec.rb index 745d051a5c1..5e0b57e9b2e 100644 --- a/spec/controllers/projects/branches_controller_spec.rb +++ b/spec/controllers/projects/branches_controller_spec.rb @@ -367,5 +367,20 @@ describe Projects::BranchesController do expect(parsed_response.first).to eq 'master' end end + + context 'when branch contains an invalid UTF-8 sequence' do + before do + project.repository.create_branch("wrong-\xE5-utf8-sequence") + end + + it 'return with a status 200' do + get :index, + namespace_id: project.namespace, + project_id: project, + format: :html + + expect(response).to have_http_status(200) + end + end end end diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb index e010b5f3444..33aca6cb527 100644 --- a/spec/features/boards/boards_spec.rb +++ b/spec/features/boards/boards_spec.rb @@ -13,7 +13,7 @@ describe 'Issue Boards', js: true do project.team << [user, :master] project.team << [user2, :master] - allow_any_instance_of(ApplicationHelper).to receive(:collapsed_sidebar?).and_return(true) + page.driver.set_cookie('sidebar_collapsed', 'true') sign_in(user) end diff --git a/spec/features/dashboard/projects_spec.rb b/spec/features/dashboard/projects_spec.rb index 0613c158c54..9a7b8e3ba6b 100644 --- a/spec/features/dashboard/projects_spec.rb +++ b/spec/features/dashboard/projects_spec.rb @@ -83,12 +83,14 @@ feature 'Dashboard Projects' do end end - context 'last push widget' do + context 'last push widget', :use_clean_rails_memory_store_caching do before do event = create(:push_event, project: project, author: user) create(:push_event_payload, event: event, ref: 'feature', action: :created) + Users::LastPushEventService.new(user).cache_last_push_event(event) + visit dashboard_projects_path end diff --git a/spec/features/groups/merge_requests_spec.rb b/spec/features/groups/merge_requests_spec.rb index 2577d98df6f..7ce6a61d50c 100644 --- a/spec/features/groups/merge_requests_spec.rb +++ b/spec/features/groups/merge_requests_spec.rb @@ -25,7 +25,7 @@ feature 'Group merge requests page' do end it 'ignores archived merge request count badges in navbar' do - expect( page.find('[aria-label="Merge Requests"] span.badge.count').text).to eq("1") + expect(first(:link, text: 'Merge Requests').find('.badge').text).to eq("1") end it 'ignores archived merge request count badges in state-filters' do diff --git a/spec/features/groups/user_sees_users_dropdowns_in_issuables_list_spec.rb b/spec/features/groups/user_sees_users_dropdowns_in_issuables_list_spec.rb new file mode 100644 index 00000000000..5ed4f3ad2bc --- /dev/null +++ b/spec/features/groups/user_sees_users_dropdowns_in_issuables_list_spec.rb @@ -0,0 +1,22 @@ +require 'spec_helper' + +feature 'Groups > User sees users dropdowns in issuables list' do + let(:entity) { create(:group) } + let(:user_in_dropdown) { create(:user) } + let!(:user_not_in_dropdown) { create(:user) } + let!(:project) { create(:project, group: entity) } + + before do + entity.add_developer(user_in_dropdown) + end + + it_behaves_like 'issuable user dropdown behaviors' do + let(:issuable) { create(:issue, project: project) } + let(:issuables_path) { issues_group_path(entity) } + end + + it_behaves_like 'issuable user dropdown behaviors' do + let(:issuable) { create(:merge_request, source_project: project) } + let(:issuables_path) { merge_requests_group_path(entity) } + end +end diff --git a/spec/features/issues/filtered_search/visual_tokens_spec.rb b/spec/features/issues/filtered_search/visual_tokens_spec.rb index 4ae54fd6f4e..2b624f4842d 100644 --- a/spec/features/issues/filtered_search/visual_tokens_spec.rb +++ b/spec/features/issues/filtered_search/visual_tokens_spec.rb @@ -28,7 +28,7 @@ describe 'Visual tokens', js: true do sign_in(user) create(:issue, project: project) - allow_any_instance_of(ApplicationHelper).to receive(:collapsed_sidebar?).and_return(true) + page.driver.set_cookie('sidebar_collapsed', 'true') visit project_issues_path(project) end diff --git a/spec/features/merge_requests/diff_notes_avatars_spec.rb b/spec/features/merge_requests/diff_notes_avatars_spec.rb index ca536f2800c..9bcb78d5206 100644 --- a/spec/features/merge_requests/diff_notes_avatars_spec.rb +++ b/spec/features/merge_requests/diff_notes_avatars_spec.rb @@ -22,7 +22,7 @@ feature 'Diff note avatars', js: true do project.team << [user, :master] sign_in user - allow_any_instance_of(ApplicationHelper).to receive(:collapsed_sidebar?).and_return(true) + page.driver.set_cookie('sidebar_collapsed', 'true') end context 'discussion tab' do diff --git a/spec/features/merge_requests/user_posts_diff_notes_spec.rb b/spec/features/merge_requests/user_posts_diff_notes_spec.rb index 442ce14eb7e..2fb6d0b965f 100644 --- a/spec/features/merge_requests/user_posts_diff_notes_spec.rb +++ b/spec/features/merge_requests/user_posts_diff_notes_spec.rb @@ -6,7 +6,7 @@ feature 'Merge requests > User posts diff notes', :js do let(:project) { merge_request.source_project } before do - allow_any_instance_of(ApplicationHelper).to receive(:collapsed_sidebar?).and_return(true) + page.driver.set_cookie('sidebar_collapsed', 'true') project.add_developer(user) sign_in(user) diff --git a/spec/features/milestones/show_spec.rb b/spec/features/milestones/show_spec.rb index 624f13922ed..50c5e0bb65f 100644 --- a/spec/features/milestones/show_spec.rb +++ b/spec/features/milestones/show_spec.rb @@ -18,9 +18,9 @@ describe 'Milestone show' do it 'avoids N+1 database queries' do create(:labeled_issue, issue_params) - control_count = ActiveRecord::QueryRecorder.new { visit_milestone }.count + control = ActiveRecord::QueryRecorder.new { visit_milestone } create_list(:labeled_issue, 10, issue_params) - expect { visit_milestone }.not_to exceed_query_limit(control_count) + expect { visit_milestone }.not_to exceed_query_limit(control) end end diff --git a/spec/features/profiles/user_manages_emails_spec.rb b/spec/features/profiles/user_manages_emails_spec.rb new file mode 100644 index 00000000000..7283c76eb54 --- /dev/null +++ b/spec/features/profiles/user_manages_emails_spec.rb @@ -0,0 +1,78 @@ +require 'spec_helper' + +describe 'User manages emails' do + let(:user) { create(:user) } + + before do + sign_in(user) + + visit(profile_emails_path) + end + + it "shows user's emails" do + expect(page).to have_content(user.email) + + user.emails.each do |email| + expect(page).to have_content(email.email) + end + end + + it 'adds an email' do + fill_in('email_email', with: 'my@email.com') + click_button('Add') + + email = user.emails.find_by(email: 'my@email.com') + + expect(email).not_to be_nil + expect(page).to have_content('my@email.com') + expect(page).to have_content(user.email) + + user.emails.each do |email| + expect(page).to have_content(email.email) + end + end + + it 'does not add a duplicate email' do + fill_in('email_email', with: user.email) + click_button('Add') + + email = user.emails.find_by(email: user.email) + + expect(email).to be_nil + expect(page).to have_content(user.email) + + user.emails.each do |email| + expect(page).to have_content(email.email) + end + end + + it 'removes an email' do + fill_in('email_email', with: 'my@email.com') + click_button('Add') + + email = user.emails.find_by(email: 'my@email.com') + + expect(email).not_to be_nil + expect(page).to have_content('my@email.com') + expect(page).to have_content(user.email) + + user.emails.each do |email| + expect(page).to have_content(email.email) + end + + # There should be only one remove button at this time + click_link('Remove') + + # Force these to reload as they have been cached + user.emails.reload + email = user.emails.find_by(email: 'my@email.com') + + expect(email).to be_nil + expect(page).not_to have_content('my@email.com') + expect(page).to have_content(user.email) + + user.emails.each do |email| + expect(page).to have_content(email.email) + end + end +end diff --git a/spec/features/profiles/user_visits_profile_account_page_spec.rb b/spec/features/profiles/user_visits_profile_account_page_spec.rb new file mode 100644 index 00000000000..8c7233c77ad --- /dev/null +++ b/spec/features/profiles/user_visits_profile_account_page_spec.rb @@ -0,0 +1,16 @@ +require 'spec_helper' + +describe 'User visits the profile account page' do + let(:user) { create(:user) } + + before do + sign_in(user) + + visit(profile_account_path) + end + + it 'shows correct menu item' do + expect(find('.sidebar-top-level-items > li.active')).to have_content('Account') + expect(page).to have_selector('.sidebar-top-level-items > li.active', count: 1) + end +end diff --git a/spec/features/profiles/user_visits_profile_authentication_log_page_spec.rb b/spec/features/profiles/user_visits_profile_authentication_log_page_spec.rb new file mode 100644 index 00000000000..ffb504cc573 --- /dev/null +++ b/spec/features/profiles/user_visits_profile_authentication_log_page_spec.rb @@ -0,0 +1,16 @@ +require 'spec_helper' + +describe 'User visits the authentication log page' do + let(:user) { create(:user) } + + before do + sign_in(user) + + visit(audit_log_profile_path) + end + + it 'shows correct menu item' do + expect(find('.sidebar-top-level-items > li.active')).to have_content('Authentication log') + expect(page).to have_selector('.sidebar-top-level-items > li.active', count: 1) + end +end diff --git a/spec/features/profiles/user_visits_profile_page_spec.rb b/spec/features/profiles/user_visits_profile_page_spec.rb new file mode 100644 index 00000000000..3bf6d718bc7 --- /dev/null +++ b/spec/features/profiles/user_visits_profile_page_spec.rb @@ -0,0 +1,16 @@ +require 'spec_helper' + +describe 'User visits the profile page' do + let(:user) { create(:user) } + + before do + sign_in(user) + + visit(profile_path) + end + + it 'shows correct menu item' do + expect(find('.sidebar-top-level-items > li.active')).to have_content('Profile') + expect(page).to have_selector('.sidebar-top-level-items > li.active', count: 1) + end +end diff --git a/spec/features/profiles/preferences_spec.rb b/spec/features/profiles/user_visits_profile_preferences_page_spec.rb index c935cdfd5c4..d1776b3d7c2 100644 --- a/spec/features/profiles/preferences_spec.rb +++ b/spec/features/profiles/user_visits_profile_preferences_page_spec.rb @@ -1,14 +1,20 @@ require 'spec_helper' -describe 'Profile > Preferences', :js do +describe 'User visits the profile preferences page' do let(:user) { create(:user) } before do sign_in(user) - visit profile_preferences_path + + visit(profile_preferences_path) + end + + it 'shows correct menu item' do + expect(find('.sidebar-top-level-items > li.active')).to have_content('Preferences') + expect(page).to have_selector('.sidebar-top-level-items > li.active', count: 1) end - describe 'User changes their syntax highlighting theme' do + describe 'User changes their syntax highlighting theme', :js do it 'creates a flash message' do choose 'user_color_scheme_id_5' @@ -27,7 +33,7 @@ describe 'Profile > Preferences', :js do end end - describe 'User changes their default dashboard' do + describe 'User changes their default dashboard', :js do it 'creates a flash message' do select 'Starred Projects', from: 'user_dashboard' click_button 'Save' diff --git a/spec/features/profiles/user_visits_profile_ssh_keys_page_spec.rb b/spec/features/profiles/user_visits_profile_ssh_keys_page_spec.rb new file mode 100644 index 00000000000..0b7a63b54b4 --- /dev/null +++ b/spec/features/profiles/user_visits_profile_ssh_keys_page_spec.rb @@ -0,0 +1,16 @@ +require 'spec_helper' + +describe 'User visits the profile SSH keys page' do + let(:user) { create(:user) } + + before do + sign_in(user) + + visit(profile_keys_path) + end + + it 'shows correct menu item' do + expect(find('.sidebar-top-level-items > li.active')).to have_content('SSH Keys') + expect(page).to have_selector('.sidebar-top-level-items > li.active', count: 1) + end +end diff --git a/spec/features/projects/awards/user_interacts_with_awards_in_issue_spec.rb b/spec/features/projects/awards/user_interacts_with_awards_in_issue_spec.rb new file mode 100644 index 00000000000..adff0a10f0e --- /dev/null +++ b/spec/features/projects/awards/user_interacts_with_awards_in_issue_spec.rb @@ -0,0 +1,104 @@ +require 'spec_helper' + +describe 'User interacts with awards in an issue', :js do + let(:issue) { create(:issue, project: project)} + let(:project) { create(:project) } + let(:user) { create(:user) } + + before do + project.add_master(user) + sign_in(user) + + visit(project_issue_path(project, issue)) + end + + it 'toggles the thumbsup award emoji' do + page.within('.awards') do + thumbsup = page.first('.award-control') + thumbsup.click + thumbsup.hover + + expect(page).to have_selector('.js-emoji-btn') + expect(page).to have_css(".js-emoji-btn.active[data-original-title='You']") + expect(page.find('.js-emoji-btn.active .js-counter')).to have_content('1') + + thumbsup = page.first('.award-control') + thumbsup.click + thumbsup.hover + + expect(page).to have_selector('.award-control.js-emoji-btn') + expect(page.all('.award-control.js-emoji-btn').size).to eq(2) + + page.all('.award-control.js-emoji-btn').each do |element| + expect(element['title']).to eq('') + end + + page.all('.award-control .js-counter').each do |element| + expect(element).to have_content('0') + end + + thumbsup = page.first('.award-control') + thumbsup.click + thumbsup.hover + + expect(page).to have_selector('.js-emoji-btn') + expect(page).to have_css(".js-emoji-btn.active[data-original-title='You']") + expect(page.find('.js-emoji-btn.active .js-counter')).to have_content('1') + end + end + + it 'toggles a custom award emoji' do + page.within('.awards') do + page.find('.js-add-award').click + end + + page.find('.emoji-menu.is-visible') + + expect(page).to have_selector('.js-emoji-menu-search') + expect(page.evaluate_script("document.activeElement.classList.contains('js-emoji-menu-search')")).to eq(true) + + page.within('.emoji-menu-content') do + emoji_button = page.first('.js-emoji-btn') + emoji_button.hover + emoji_button.click + end + + page.within('.awards') do + expect(page).to have_selector('.js-emoji-btn') + expect(page.find('.js-emoji-btn.active .js-counter')).to have_content('1') + expect(page).to have_css(".js-emoji-btn.active[data-original-title='You']") + + expect do + page.find('.js-emoji-btn.active').click + wait_for_requests + end.to change { page.all('.award-control.js-emoji-btn').size }.from(3).to(2) + end + end + + it 'shows the list of award emoji categories' do + page.within('.awards') do + page.find('.js-add-award').click + end + + page.find('.emoji-menu.is-visible') + + expect(page).to have_selector('.js-emoji-menu-search') + expect(page.evaluate_script("document.activeElement.classList.contains('js-emoji-menu-search')")).to eq(true) + + fill_in('emoji-menu-search', with: 'hand') + + page.within('.emoji-menu-content') do + expect(page).to have_selector('[data-name="raised_hand"]') + end + end + + it 'adds an award emoji by a comment' do + page.within('.js-main-target-form') do + fill_in('note[note]', with: ':smile:') + + click_button('Comment') + end + + expect(page).to have_selector('gl-emoji[data-name="smile"]') + end +end diff --git a/spec/features/projects/diffs/diff_show_spec.rb b/spec/features/projects/diffs/diff_show_spec.rb index bc102895aaf..a6f52c9ef58 100644 --- a/spec/features/projects/diffs/diff_show_spec.rb +++ b/spec/features/projects/diffs/diff_show_spec.rb @@ -108,6 +108,19 @@ feature 'Diff file viewer', :js do end end + context 'renamed file' do + before do + visit_commit('6907208d755b60ebeacb2e9dfea74c92c3449a1f') + end + + it 'shows the filename with diff highlight' do + within('.file-header-content') do + expect(page).to have_css('.idiff.left.right.deletion') + expect(page).to have_content('files/js/commit.coffee') + end + end + end + context 'binary file that appears to be text in the first 1024 bytes' do before do # The file we're visiting is smaller than 10 KB and we want it collapsed diff --git a/spec/features/projects/jobs/user_browses_job_spec.rb b/spec/features/projects/jobs/user_browses_job_spec.rb new file mode 100644 index 00000000000..21c9acc7ac0 --- /dev/null +++ b/spec/features/projects/jobs/user_browses_job_spec.rb @@ -0,0 +1,37 @@ +require 'spec_helper' + +describe 'User browses a job', :js do + let!(:build) { create(:ci_build, :coverage, pipeline: pipeline) } + let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.sha, ref: 'master') } + let(:project) { create(:project, :repository, namespace: user.namespace) } + let(:user) { create(:user) } + + before do + project.add_master(user) + project.enable_ci + build.success + build.trace.set('job trace') + + sign_in(user) + + visit(project_job_path(project, build)) + end + + it 'erases the job log' do + expect(page).to have_content("Job ##{build.id}") + expect(page).to have_css('#build-trace') + + click_link('Erase') + + expect(build).not_to have_trace + expect(build.artifacts_file.exists?).to be_falsy + expect(build.artifacts_metadata.exists?).to be_falsy + expect(page).to have_no_css('.artifacts') + + page.within('.erased') do + expect(page).to have_content('Job has been erased') + end + + expect(build.project.running_or_pending_build_count).to eq(build.project.builds.running_or_pending.count(:all)) + end +end diff --git a/spec/features/projects/jobs/user_browses_jobs_spec.rb b/spec/features/projects/jobs/user_browses_jobs_spec.rb new file mode 100644 index 00000000000..767777f3bf9 --- /dev/null +++ b/spec/features/projects/jobs/user_browses_jobs_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +describe 'User browses jobs' do + let!(:build) { create(:ci_build, :coverage, pipeline: pipeline) } + let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.sha, ref: 'master') } + let(:project) { create(:project, :repository, namespace: user.namespace) } + let(:user) { create(:user) } + + before do + project.add_master(user) + project.enable_ci + project.update_attribute(:build_coverage_regex, /Coverage (\d+)%/) + + sign_in(user) + + visit(project_jobs_path(project)) + end + + it 'shows the coverage' do + page.within('td.coverage') do + expect(page).to have_content('99.9%') + end + end + + it 'shows the "CI Lint" button' do + page.within('.nav-controls') do + ci_lint_tool_link = page.find_link('CI lint') + + expect(ci_lint_tool_link[:href]).to end_with(ci_lint_path) + end + end +end diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb index 3b5c6966287..a4ed589f3de 100644 --- a/spec/features/projects/jobs_spec.rb +++ b/spec/features/projects/jobs_spec.rb @@ -164,9 +164,9 @@ feature 'Jobs' do end it 'links to issues/new with the title and description filled in' do - button_title = "Build Failed ##{job.id}" - job_path = project_job_path(project, job) - options = { issue: { title: button_title, description: job_path } } + button_title = "Job Failed ##{job.id}" + job_url = project_job_path(project, job) + options = { issue: { title: button_title, description: "Job [##{job.id}](#{job_url}) failed for #{job.sha}:\n" } } href = new_project_issue_path(project, options) diff --git a/spec/features/projects/merge_requests/user_accepts_merge_request_spec.rb b/spec/features/projects/merge_requests/user_accepts_merge_request_spec.rb new file mode 100644 index 00000000000..6c0b5e279d5 --- /dev/null +++ b/spec/features/projects/merge_requests/user_accepts_merge_request_spec.rb @@ -0,0 +1,65 @@ +require 'spec_helper' + +describe 'User accepts a merge request', :js do + let(:merge_request) { create(:merge_request, :with_diffs, :simple, source_project: project) } + let(:project) { create(:project, :public, :repository) } + let(:user) { create(:user) } + + before do + project.add_developer(user) + sign_in(user) + end + + context 'with removing the source branch' do + before do + visit(merge_request_path(merge_request)) + end + + it 'accepts a merge request' do + check('Remove source branch') + click_button('Merge') + + expect(page).to have_content('The changes were merged into') + expect(page).not_to have_selector('.js-remove-branch-button') + + # Wait for View Resource requests to complete so they don't blow up if they are + # only handled after `DatabaseCleaner` has already run. + wait_for_requests + end + end + + context 'without removing the source branch' do + before do + visit(merge_request_path(merge_request)) + end + + it 'accepts a merge request' do + click_button('Merge') + + expect(page).to have_content('The changes were merged into') + expect(page).to have_selector('.js-remove-branch-button') + + # Wait for View Resource requests to complete so they don't blow up if they are + # only handled after `DatabaseCleaner` has already run + wait_for_requests + end + end + + context 'when a URL has an anchor' do + before do + visit(merge_request_path(merge_request, anchor: 'note_123')) + end + + it 'accepts a merge request' do + check('Remove source branch') + click_button('Merge') + + expect(page).to have_content('The changes were merged into') + expect(page).not_to have_selector('.js-remove-branch-button') + + # Wait for View Resource requests to complete so they don't blow up if they are + # only handled after `DatabaseCleaner` has already run + wait_for_requests + end + end +end diff --git a/spec/features/projects/merge_requests/user_reverts_merge_request_spec.rb b/spec/features/projects/merge_requests/user_reverts_merge_request_spec.rb new file mode 100644 index 00000000000..a41d683dbbb --- /dev/null +++ b/spec/features/projects/merge_requests/user_reverts_merge_request_spec.rb @@ -0,0 +1,59 @@ +require 'spec_helper' + +describe 'User reverts a merge request', :js do + let(:merge_request) { create(:merge_request, :with_diffs, :simple, source_project: project) } + let(:project) { create(:project, :public, :repository) } + let(:user) { create(:user) } + + before do + project.add_developer(user) + sign_in(user) + + visit(merge_request_path(merge_request)) + + click_button('Merge') + + visit(merge_request_path(merge_request)) + end + + it 'reverts a merge request' do + find("a[href='#modal-revert-commit']").click + + page.within('#modal-revert-commit') do + uncheck('create_merge_request') + click_button('Revert') + end + + expect(page).to have_content('The merge request has been successfully reverted.') + + wait_for_requests + end + + it 'does not revert a merge request that was previously reverted' do + find("a[href='#modal-revert-commit']").click + + page.within('#modal-revert-commit') do + uncheck('create_merge_request') + click_button('Revert') + end + + find("a[href='#modal-revert-commit']").click + + page.within('#modal-revert-commit') do + uncheck('create_merge_request') + click_button('Revert') + end + + expect(page).to have_content('Sorry, we cannot revert this merge request automatically.') + end + + it 'reverts a merge request in a new merge request' do + find("a[href='#modal-revert-commit']").click + + page.within('#modal-revert-commit') do + click_button('Revert') + end + + expect(page).to have_content('The merge request has been successfully reverted. You can now submit a merge request to get this change into the original branch.') + end +end diff --git a/spec/features/projects/milestones/user_interacts_with_labels_spec.rb b/spec/features/projects/milestones/user_interacts_with_labels_spec.rb new file mode 100644 index 00000000000..f6a82f80d65 --- /dev/null +++ b/spec/features/projects/milestones/user_interacts_with_labels_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +describe 'User interacts with labels' do + let(:user) { create(:user) } + let(:project) { create(:project, namespace: user.namespace) } + let(:milestone) { create(:milestone, project: project, title: 'v2.2', description: '# Description header') } + let(:issue1) { create(:issue, project: project, title: 'Bugfix1', milestone: milestone) } + let(:issue2) { create(:issue, project: project, title: 'Bugfix2', milestone: milestone) } + let(:label_bug) { create(:label, project: project, title: 'bug') } + let(:label_feature) { create(:label, project: project, title: 'feature') } + let(:label_enhancement) { create(:label, project: project, title: 'enhancement') } + + before do + project.add_master(user) + sign_in(user) + + issue1.labels << [label_bug, label_feature] + issue2.labels << [label_bug, label_enhancement] + + visit(project_milestones_path(project)) + end + + it 'shows the list of labels', :js do + click_link('v2.2') + + page.within('.nav-sidebar') do + page.find(:xpath, "//a[@href='#tab-labels']").click + end + + expect(page).to have_selector('ul.manage-labels-list') + + wait_for_requests + + page.within('#tab-labels') do + expect(page).to have_content(label_bug.title) + expect(page).to have_content(label_enhancement.title) + expect(page).to have_content(label_feature.title) + end + end +end diff --git a/spec/features/projects/settings/user_manages_group_links_spec.rb b/spec/features/projects/settings/user_manages_group_links_spec.rb new file mode 100644 index 00000000000..91e8059865c --- /dev/null +++ b/spec/features/projects/settings/user_manages_group_links_spec.rb @@ -0,0 +1,41 @@ +require 'spec_helper' + +describe 'User manages group links' do + include Select2Helper + + let(:user) { create(:user) } + let(:project) { create(:project, namespace: user.namespace) } + let(:group_ops) { create(:group, name: 'Ops') } + let(:group_market) { create(:group, name: 'Market', path: 'market') } + + before do + project.add_master(user) + sign_in(user) + + share_link = project.project_group_links.new(group_access: Gitlab::Access::MASTER) + share_link.group_id = group_ops.id + share_link.save! + + visit(project_group_links_path(project)) + end + + it 'shows a list of groups' do + page.within('.project-members-groups') do + expect(page).to have_content('Ops') + expect(page).not_to have_content('Market') + end + end + + it 'shares a project with a group', :js do + click_link('Share with group') + + select2(group_market.id, from: '#link_group_id') + select('Master', from: 'link_group_access') + + click_button('Share') + + page.within('.project-members-groups') do + expect(page).to have_content('Market') + end + end +end diff --git a/spec/features/projects/settings/user_manages_project_members_spec.rb b/spec/features/projects/settings/user_manages_project_members_spec.rb new file mode 100644 index 00000000000..2709047b8de --- /dev/null +++ b/spec/features/projects/settings/user_manages_project_members_spec.rb @@ -0,0 +1,68 @@ +require 'spec_helper' + +describe 'User manages project members' do + let(:group) { create(:group, name: 'OpenSource') } + let(:project) { create(:project) } + let(:project2) { create(:project) } + let(:user) { create(:user) } + let(:user_dmitriy) { create(:user, name: 'Dmitriy') } + let(:user_mike) { create(:user, name: 'Mike') } + + before do + project.add_master(user) + project.add_developer(user_dmitriy) + sign_in(user) + end + + it 'cancels a team member' do + visit(project_project_members_path(project)) + + project_member = project.project_members.find_by(user_id: user_dmitriy.id) + + page.within("#project_member_#{project_member.id}") do + click_link('Remove user from project') + end + + visit(project_project_members_path(project)) + + expect(page).not_to have_content(user_dmitriy.name) + expect(page).not_to have_content(user_dmitriy.username) + end + + it 'imports a team from another project' do + project2.add_master(user) + project2.add_reporter(user_mike) + + visit(project_project_members_path(project)) + + page.within('.users-project-form') do + click_link('Import') + end + + select(project2.name_with_namespace, from: 'source_project_id') + click_button('Import') + + project_member = project.project_members.find_by(user_id: user_mike.id) + + page.within("#project_member_#{project_member.id}") do + expect(page).to have_content('Mike') + expect(page).to have_content('Reporter') + end + end + + it 'shows all members of project shared group' do + group.add_owner(user) + group.add_developer(user_dmitriy) + + share_link = project.project_group_links.new(group_access: Gitlab::Access::MASTER) + share_link.group_id = group.id + share_link.save! + + visit(project_project_members_path(project)) + + page.within('.project-members-groups') do + expect(page).to have_content('OpenSource') + expect(first('.group_member')).to have_content('Master') + end + end +end diff --git a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb index 9d66f482c8d..e72b7dc0dd5 100644 --- a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb +++ b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb @@ -1,38 +1,75 @@ require 'spec_helper' -feature 'Projects > Wiki > User creates wiki page', :js do +describe 'User creates wiki page' do let(:user) { create(:user) } - background do - project.team << [user, :master] + before do + project.add_master(user) sign_in(user) - visit project_path(project) + visit(project_wikis_path(project)) end - context 'in the user namespace' do - let(:project) { create(:project, namespace: user.namespace) } + context 'when wiki is empty' do + context 'in a user namespace' do + let(:project) { create(:project, namespace: user.namespace) } - context 'when wiki is empty' do - before do - find('.shortcuts-wiki').trigger('click') + it 'shows validation error message' do + page.within('.wiki-form') do + fill_in(:wiki_content, with: '') + click_on('Create page') + end + + expect(page).to have_content('The form contains the following error:') + expect(page).to have_content("Content can't be blank") + + page.within('.wiki-form') do + fill_in(:wiki_content, with: '[link test](test)') + click_on('Create page') + end + + expect(page).to have_content('Home') + expect(page).to have_content('link test') + + click_link('link test') + + expect(page).to have_content('Create Page') + end + + it 'shows non-escaped link in the pages list', :js do + click_link('New page') + + page.within('#modal-new-wiki') do + fill_in(:new_wiki_path, with: 'one/two/three-test') + click_on('Create page') + end + + page.within('.wiki-form') do + fill_in(:wiki_content, with: 'wiki content') + click_on('Create page') + end + + expect(current_path).to include('one/two/three-test') + expect(page).to have_xpath("//a[@href='/#{project.full_path}/wikis/one/two/three-test']") end - scenario 'commit message field has value "Create home"' do + it 'has "Create home" as a commit message' do expect(page).to have_field('wiki[message]', with: 'Create home') end - scenario 'directly from the wiki home page' do - fill_in :wiki_content, with: 'My awesome wiki!' - page.within '.wiki-form' do - click_button 'Create page' + it 'creates a page from the home page' do + fill_in(:wiki_content, with: 'My awesome wiki!') + + page.within('.wiki-form') do + click_button('Create page') end + expect(page).to have_content('Home') expect(page).to have_content("Last edited by #{user.name}") expect(page).to have_content('My awesome wiki!') end - scenario 'creates ASCII wiki with LaTeX blocks' do + it 'creates ASCII wiki with LaTeX blocks', :js do stub_application_setting(plantuml_url: 'http://localhost', plantuml_enabled: true) ascii_content = <<~MD @@ -54,10 +91,10 @@ feature 'Projects > Wiki > User creates wiki page', :js do MD find('#wiki_format option[value=asciidoc]').select_option - fill_in :wiki_content, with: ascii_content + fill_in(:wiki_content, with: ascii_content) - page.within '.wiki-form' do - click_button 'Create page' + page.within('.wiki-form') do + click_button('Create page') end page.within '.wiki' do @@ -67,27 +104,49 @@ feature 'Projects > Wiki > User creates wiki page', :js do end end - context 'when wiki is not empty' do - before do - WikiPages::CreateService.new(project, user, title: 'home', content: 'Home page').execute - find('.shortcuts-wiki').trigger('click') + context 'in a group namespace', :js do + let(:project) { create(:project, namespace: create(:group, :public)) } + + it 'has "Create home" as a commit message' do + expect(page).to have_field('wiki[message]', with: 'Create home') + end + + it 'creates a page from from the home page' do + page.within('.wiki-form') do + fill_in(:wiki_content, with: 'My awesome wiki!') + click_button('Create page') + end + + expect(page).to have_content('Home') + expect(page).to have_content("Last edited by #{user.name}") + expect(page).to have_content('My awesome wiki!') end + end + end + + context 'when wiki is not empty', :js do + before do + create(:wiki_page, wiki: create(:project, namespace: user.namespace).wiki, attrs: { title: 'home', content: 'Home page' }) + end + + context 'in a user namespace' do + let(:project) { create(:project, namespace: user.namespace) } context 'via the "new wiki page" page' do - scenario 'when the wiki page has a single word name' do - click_link 'New page' + it 'creates a page with a single word' do + click_link('New page') - page.within '#modal-new-wiki' do - fill_in :new_wiki_path, with: 'foo' - click_button 'Create page' + page.within('#modal-new-wiki') do + fill_in(:new_wiki_path, with: 'foo') + click_button('Create page') end # Commit message field should have correct value. expect(page).to have_field('wiki[message]', with: 'Create foo') - page.within '.wiki-form' do - fill_in :wiki_content, with: 'My awesome wiki!' - click_button 'Create page' + page.within('.wiki-form') do + fill_in(:wiki_content, with: 'My awesome wiki!') + click_button('Create page') end expect(page).to have_content('Foo') @@ -95,20 +154,20 @@ feature 'Projects > Wiki > User creates wiki page', :js do expect(page).to have_content('My awesome wiki!') end - scenario 'when the wiki page has spaces in the name' do - click_link 'New page' + it 'creates a page with spaces in the name' do + click_link('New page') - page.within '#modal-new-wiki' do - fill_in :new_wiki_path, with: 'Spaces in the name' - click_button 'Create page' + page.within('#modal-new-wiki') do + fill_in(:new_wiki_path, with: 'Spaces in the name') + click_button('Create page') end # Commit message field should have correct value. expect(page).to have_field('wiki[message]', with: 'Create spaces in the name') - page.within '.wiki-form' do - fill_in :wiki_content, with: 'My awesome wiki!' - click_button 'Create page' + page.within('.wiki-form') do + fill_in(:wiki_content, with: 'My awesome wiki!') + click_button('Create page') end expect(page).to have_content('Spaces in the name') @@ -116,20 +175,20 @@ feature 'Projects > Wiki > User creates wiki page', :js do expect(page).to have_content('My awesome wiki!') end - scenario 'when the wiki page has hyphens in the name' do - click_link 'New page' + it 'creates a page with hyphens in the name' do + click_link('New page') - page.within '#modal-new-wiki' do - fill_in :new_wiki_path, with: 'hyphens-in-the-name' - click_button 'Create page' + page.within('#modal-new-wiki') do + fill_in(:new_wiki_path, with: 'hyphens-in-the-name') + click_button('Create page') end # Commit message field should have correct value. expect(page).to have_field('wiki[message]', with: 'Create hyphens in the name') - page.within '.wiki-form' do - fill_in :wiki_content, with: 'My awesome wiki!' - click_button 'Create page' + page.within('.wiki-form') do + fill_in(:wiki_content, with: 'My awesome wiki!') + click_button('Create page') end expect(page).to have_content('Hyphens in the name') @@ -138,73 +197,47 @@ feature 'Projects > Wiki > User creates wiki page', :js do end end - scenario 'content has autocomplete' do - click_link 'New page' + it 'shows the autocompletion dropdown' do + click_link('New page') - page.within '#modal-new-wiki' do - fill_in :new_wiki_path, with: 'test-autocomplete' - click_button 'Create page' + page.within('#modal-new-wiki') do + fill_in(:new_wiki_path, with: 'test-autocomplete') + click_button('Create page') end - page.within '.wiki-form' do + page.within('.wiki-form') do find('#wiki_content').native.send_keys('') - fill_in :wiki_content, with: '@' + fill_in(:wiki_content, with: '@') end expect(page).to have_selector('.atwho-view') end end - end - - context 'in a group namespace' do - let(:project) { create(:project, namespace: create(:group, :public)) } - context 'when wiki is empty' do - before do - find('.shortcuts-wiki').trigger('click') - end - - scenario 'commit message field has value "Create home"' do - expect(page).to have_field('wiki[message]', with: 'Create home') - end + context 'in a group namespace' do + let(:project) { create(:project, namespace: create(:group, :public)) } - scenario 'directly from the wiki home page' do - fill_in :wiki_content, with: 'My awesome wiki!' - page.within '.wiki-form' do - click_button 'Create page' - end - - expect(page).to have_content('Home') - expect(page).to have_content("Last edited by #{user.name}") - expect(page).to have_content('My awesome wiki!') - end - end - - context 'when wiki is not empty' do - before do - WikiPages::CreateService.new(project, user, title: 'home', content: 'Home page').execute - find('.shortcuts-wiki').trigger('click') - end + context 'via the "new wiki page" page' do + it 'creates a page' do + click_link('New page') - scenario 'via the "new wiki page" page' do - click_link 'New page' + page.within('#modal-new-wiki') do + fill_in(:new_wiki_path, with: 'foo') + click_button('Create page') + end - page.within '#modal-new-wiki' do - fill_in :new_wiki_path, with: 'foo' - click_button 'Create page' - end + # Commit message field should have correct value. + expect(page).to have_field('wiki[message]', with: 'Create foo') - # Commit message field should have correct value. - expect(page).to have_field('wiki[message]', with: 'Create foo') + page.within('.wiki-form') do + fill_in(:wiki_content, with: 'My awesome wiki!') + click_button('Create page') + end - page.within '.wiki-form' do - fill_in :wiki_content, with: 'My awesome wiki!' - click_button 'Create page' + expect(page).to have_content('Foo') + expect(page).to have_content("Last edited by #{user.name}") + expect(page).to have_content('My awesome wiki!') end - - expect(page).to have_content('Foo') - expect(page).to have_content("Last edited by #{user.name}") - expect(page).to have_content('My awesome wiki!') end end end diff --git a/spec/features/projects/wiki/user_deletes_wiki_page_spec.rb b/spec/features/projects/wiki/user_deletes_wiki_page_spec.rb new file mode 100644 index 00000000000..605e332196b --- /dev/null +++ b/spec/features/projects/wiki/user_deletes_wiki_page_spec.rb @@ -0,0 +1,19 @@ +require 'spec_helper' + +feature 'User deletes wiki page' do + let(:user) { create(:user) } + let(:project) { create(:project, namespace: user.namespace) } + let(:wiki_page) { create(:wiki_page, wiki: project.wiki) } + + before do + sign_in(user) + visit(project_wiki_path(project, wiki_page)) + end + + it 'deletes a page' do + click_on('Edit') + click_on('Delete') + + expect(page).to have_content('Page was successfully deleted') + end +end diff --git a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb index 64a80aec205..1cf14204159 100644 --- a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb +++ b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb @@ -1,83 +1,154 @@ require 'spec_helper' -feature 'Projects > Wiki > User updates wiki page' do +describe 'User updates wiki page' do let(:user) { create(:user) } - let!(:wiki_page) { WikiPages::CreateService.new(project, user, title: 'home', content: 'Home page').execute } - background do - project.team << [user, :master] + before do + project.add_master(user) sign_in(user) + end + + context 'when wiki is empty' do + before do + visit(project_wikis_path(project)) + end + + context 'in a user namespace' do + let(:project) { create(:project, namespace: user.namespace) } + + it 'redirects back to the home edit page' do + page.within(:css, '.wiki-form .form-actions') do + click_on('Cancel') + end + + expect(current_path).to eq project_wiki_path(project, :home) + end + + it 'updates a page that has a path', :js do + click_on('New page') + + page.within('#modal-new-wiki') do + fill_in(:new_wiki_path, with: 'one/two/three-test') + click_on('Create page') + end + + page.within '.wiki-form' do + fill_in(:wiki_content, with: 'wiki content') + click_on('Create page') + end - visit project_wikis_path(project) + expect(current_path).to include('one/two/three-test') + expect(find('.wiki-pages')).to have_content('Three') + + click_on('Three') + + expect(find('.nav-text')).to have_content('Three') + + click_on('Edit') + + expect(current_path).to include('one/two/three-test') + expect(page).to have_content('Edit Page') + + fill_in('Content', with: 'Updated Wiki Content') + click_on('Save changes') + + expect(page).to have_content('Updated Wiki Content') + end + end end - context 'in the user namespace' do - let(:project) { create(:project, namespace: user.namespace) } + context 'when wiki is not empty' do + let(:project_wiki) { create(:project_wiki, project: project, user: project.creator) } + let!(:wiki_page) { create(:wiki_page, wiki: project_wiki, attrs: { title: 'home', content: 'Home page' }) } - context 'the home page' do - scenario 'success when the wiki content is not empty' do - click_link 'Edit' + before do + visit(project_wikis_path(project)) + end + + context 'in a user namespace' do + let(:project) { create(:project, namespace: user.namespace) } + + it 'updates a page' do + click_link('Edit') # Commit message field should have correct value. expect(page).to have_field('wiki[message]', with: 'Update home') - fill_in :wiki_content, with: 'My awesome wiki!' - click_button 'Save changes' + fill_in(:wiki_content, with: 'My awesome wiki!') + click_button('Save changes') expect(page).to have_content('Home') expect(page).to have_content("Last edited by #{user.name}") expect(page).to have_content('My awesome wiki!') end - scenario 'failure when the wiki content is empty' do - click_link 'Edit' + it 'shows a validation error message' do + click_link('Edit') - fill_in :wiki_content, with: '' - click_button 'Save changes' + fill_in(:wiki_content, with: '') + click_button('Save changes') expect(page).to have_selector('.wiki-form') expect(page).to have_content('Edit Page') expect(page).to have_content('The form contains the following error:') - expect(page).to have_content('Content can\'t be blank') - expect(find('textarea#wiki_content').value).to eq '' + expect(page).to have_content("Content can't be blank") + expect(find('textarea#wiki_content').value).to eq('') end - scenario 'content has autocomplete', :js do - click_link 'Edit' + it 'shows the autocompletion dropdown', :js do + click_link('Edit') find('#wiki_content').native.send_keys('') - fill_in :wiki_content, with: '@' + fill_in(:wiki_content, with: '@') expect(page).to have_selector('.atwho-view') end - end - scenario 'page has been updated since the user opened the edit page' do - click_link 'Edit' + it 'shows the error message' do + click_link('Edit') + + wiki_page.update(content: 'Update') - wiki_page.update(content: 'Update') + click_button('Save changes') + + expect(page).to have_content('Someone edited the page the same time you did.') + end + + it 'updates a page' do + click_on('Edit') + fill_in('Content', with: 'Updated Wiki Content') + click_on('Save changes') + + expect(page).to have_content('Updated Wiki Content') + end - click_button 'Save changes' + it 'cancels edititng of a page' do + click_on('Edit') - expect(page).to have_content 'Someone edited the page the same time you did.' + page.within(:css, '.wiki-form .form-actions') do + click_on('Cancel') + end + + expect(current_path).to eq(project_wiki_path(project, wiki_page)) + end end - end - context 'in a group namespace' do - let(:project) { create(:project, namespace: create(:group, :public)) } + context 'in a group namespace' do + let(:project) { create(:project, namespace: create(:group, :public)) } - scenario 'the home page' do - click_link 'Edit' + it 'updates a page' do + click_link('Edit') - # Commit message field should have correct value. - expect(page).to have_field('wiki[message]', with: 'Update home') + # Commit message field should have correct value. + expect(page).to have_field('wiki[message]', with: 'Update home') - fill_in :wiki_content, with: 'My awesome wiki!' - click_button 'Save changes' + fill_in(:wiki_content, with: 'My awesome wiki!') + click_button('Save changes') - expect(page).to have_content('Home') - expect(page).to have_content("Last edited by #{user.name}") - expect(page).to have_content('My awesome wiki!') + expect(page).to have_content('Home') + expect(page).to have_content("Last edited by #{user.name}") + expect(page).to have_content('My awesome wiki!') + end end end end diff --git a/spec/features/projects/wiki/user_views_project_wiki_page_spec.rb b/spec/features/projects/wiki/user_views_project_wiki_page_spec.rb deleted file mode 100644 index 92e96f11219..00000000000 --- a/spec/features/projects/wiki/user_views_project_wiki_page_spec.rb +++ /dev/null @@ -1,39 +0,0 @@ -require 'spec_helper' - -feature 'Projects > Wiki > User views the wiki page' do - let(:user) { create(:user) } - let(:project) { create(:project, :public) } - let(:old_page_version_id) { wiki_page.versions.last.id } - let(:wiki_page) do - WikiPages::CreateService.new( - project, - user, - title: 'home', - content: '[some link](other-page)' - ).execute - end - - background do - project.team << [user, :master] - sign_in(user) - WikiPages::UpdateService.new( - project, - user, - message: 'updated home', - content: 'updated [some link](other-page)', - format: :markdown - ).execute(wiki_page) - end - - scenario 'Visit Wiki Page Current Commit' do - visit project_wiki_path(project, wiki_page) - - expect(page).to have_selector('a.btn', text: 'Edit') - end - - scenario 'Visit Wiki Page Historical Commit' do - visit project_wiki_path(project, wiki_page, version_id: old_page_version_id) - - expect(page).not_to have_selector('a.btn', text: 'Edit') - end -end diff --git a/spec/features/projects/wiki/user_views_wiki_page_spec.rb b/spec/features/projects/wiki/user_views_wiki_page_spec.rb new file mode 100644 index 00000000000..d201d4f6b98 --- /dev/null +++ b/spec/features/projects/wiki/user_views_wiki_page_spec.rb @@ -0,0 +1,140 @@ +require 'spec_helper' + +describe 'User views a wiki page' do + let(:user) { create(:user) } + let(:project) { create(:project, namespace: user.namespace) } + let(:wiki_page) do + create(:wiki_page, + wiki: project.wiki, + attrs: { title: 'home', content: 'Look at this [image](image.jpg)\n\n ![alt text](image.jpg)' }) + end + + before do + project.add_master(user) + sign_in(user) + end + + context 'when wiki is empty' do + before do + visit(project_wikis_path(project)) + + click_on('New page') + + page.within('#modal-new-wiki') do + fill_in(:new_wiki_path, with: 'one/two/three-test') + click_on('Create page') + end + + page.within('.wiki-form') do + fill_in(:wiki_content, with: 'wiki content') + click_on('Create page') + end + end + + it 'shows the history of a page that has a path', :js do + expect(current_path).to include('one/two/three-test') + + click_on('Three') + click_on('Page history') + + expect(current_path).to include('one/two/three-test') + + page.within(:css, '.nav-text') do + expect(page).to have_content('History') + end + end + + it 'shows an old version of a page', :js do + expect(current_path).to include('one/two/three-test') + expect(find('.wiki-pages')).to have_content('Three') + + click_on('Three') + + expect(find('.nav-text')).to have_content('Three') + + click_on('Edit') + + expect(current_path).to include('one/two/three-test') + expect(page).to have_content('Edit Page') + + fill_in('Content', with: 'Updated Wiki Content') + + click_on('Save changes') + click_on('Page history') + + page.within(:css, '.nav-text') do + expect(page).to have_content('History') + end + + find('a[href*="?version_id"]') + end + end + + context 'when a page does not have history' do + before do + visit(project_wiki_path(project, wiki_page)) + end + + it 'shows all the pages' do + expect(page).to have_content(user.name) + expect(find('.wiki-pages')).to have_content(wiki_page.title.capitalize) + end + + it 'shows a file stored in a page' do + file = Gollum::File.new(project.wiki) + + allow_any_instance_of(Gollum::Wiki).to receive(:file).with('image.jpg', 'master', true).and_return(file) + allow_any_instance_of(Gollum::File).to receive(:mime_type).and_return('image/jpeg') + + expect(page).to have_xpath('//img[@data-src="image.jpg"]') + expect(page).to have_link('image', href: "#{project.wiki.wiki_base_path}/image.jpg") + + click_on('image') + + expect(current_path).to match('wikis/image.jpg') + expect(page).not_to have_xpath('/html') # Page should render the image which means there is no html involved + end + + it 'shows the creation page if file does not exist' do + expect(page).to have_link('image', href: "#{project.wiki.wiki_base_path}/image.jpg") + + click_on('image') + + expect(current_path).to match('wikis/image.jpg') + expect(page).to have_content('New Wiki Page') + expect(page).to have_content('Create page') + end + end + + context 'when a page has history' do + before do + wiki_page.update(message: 'updated home', content: 'updated [some link](other-page)') + end + + it 'shows the page history' do + visit(project_wiki_path(project, wiki_page)) + + expect(page).to have_selector('a.btn', text: 'Edit') + + click_on('Page history') + + expect(page).to have_content(user.name) + expect(page).to have_content("#{user.username} created page: home") + expect(page).to have_content('updated home') + end + + it 'does not show the "Edit" button' do + visit(project_wiki_path(project, wiki_page, version_id: wiki_page.versions.last.id)) + + expect(page).not_to have_selector('a.btn', text: 'Edit') + end + end + + it 'opens a default wiki page', :js do + visit(project_path(project)) + + find('.shortcuts-wiki').trigger('click') + + expect(page).to have_content('Home · Create Page') + end +end diff --git a/spec/fixtures/api/schemas/public_api/v4/user/login.json b/spec/fixtures/api/schemas/public_api/v4/user/login.json index 6181b3ccc86..e6c1d9c9d84 100644 --- a/spec/fixtures/api/schemas/public_api/v4/user/login.json +++ b/spec/fixtures/api/schemas/public_api/v4/user/login.json @@ -19,6 +19,7 @@ "organization", "last_sign_in_at", "confirmed_at", + "theme_id", "color_scheme_id", "projects_limit", "current_sign_in_at", diff --git a/spec/helpers/auto_devops_helper_spec.rb b/spec/helpers/auto_devops_helper_spec.rb index b6d892548ef..5e272af6073 100644 --- a/spec/helpers/auto_devops_helper_spec.rb +++ b/spec/helpers/auto_devops_helper_spec.rb @@ -10,6 +10,8 @@ describe AutoDevopsHelper do before do allow(helper).to receive(:can?).with(user, :admin_pipeline, project) { allowed } allow(helper).to receive(:current_user) { user } + + Feature.get(:auto_devops_banner_disabled).disable end subject { helper.show_auto_devops_callout?(project) } @@ -18,6 +20,14 @@ describe AutoDevopsHelper do it { is_expected.to eq(true) } end + context 'when the banner is disabled by feature flag' do + it 'allows the feature flag to disable' do + Feature.get(:auto_devops_banner_disabled).enable + + expect(subject).to be(false) + end + end + context 'when dismissed' do before do helper.request.cookies[:auto_devops_settings_dismissed] = 'true' @@ -55,5 +65,21 @@ describe AutoDevopsHelper do it { is_expected.to eq(false) } end + + context 'when master contains a .gitlab-ci.yml file' do + before do + allow(project.repository).to receive(:gitlab_ci_yml).and_return("script: ['test']") + end + + it { is_expected.to eq(false) } + end + + context 'when another service is enabled' do + before do + create(:service, project: project, category: :ci, active: true) + end + + it { is_expected.to eq(false) } + end end end diff --git a/spec/helpers/diff_helper_spec.rb b/spec/helpers/diff_helper_spec.rb index 0deea0ff6a3..f9c31ac61d8 100644 --- a/spec/helpers/diff_helper_spec.rb +++ b/spec/helpers/diff_helper_spec.rb @@ -136,9 +136,9 @@ describe DiffHelper do marked_old_line, marked_new_line = mark_inline_diffs(old_line, new_line) expect(marked_old_line).to eq(%q{abc <span class="idiff left right deletion">'def'</span>}) - expect(marked_old_line).not_to be_html_safe + expect(marked_old_line).to be_html_safe expect(marked_new_line).to eq(%q{abc <span class="idiff left right addition">"def"</span>}) - expect(marked_new_line).not_to be_html_safe + expect(marked_new_line).to be_html_safe end end diff --git a/spec/helpers/preferences_helper_spec.rb b/spec/helpers/preferences_helper_spec.rb index a04c87b08eb..8b8080563d3 100644 --- a/spec/helpers/preferences_helper_spec.rb +++ b/spec/helpers/preferences_helper_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe PreferencesHelper do - describe 'dashboard_choices' do + describe '#dashboard_choices' do it 'raises an exception when defined choices may be missing' do expect(User).to receive(:dashboards).and_return(foo: 'foo') expect { helper.dashboard_choices }.to raise_error(RuntimeError) @@ -26,7 +26,33 @@ describe PreferencesHelper do end end - describe 'user_color_scheme' do + describe '#user_application_theme' do + context 'with a user' do + it "returns user's theme's css_class" do + stub_user(theme_id: 3) + + expect(helper.user_application_theme).to eq 'ui_light' + end + + it 'returns the default when id is invalid' do + stub_user(theme_id: Gitlab::Themes.count + 5) + + allow(Gitlab.config.gitlab).to receive(:default_theme).and_return(1) + + expect(helper.user_application_theme).to eq 'ui_indigo' + end + end + + context 'without a user' do + it 'returns the default theme' do + stub_user + + expect(helper.user_application_theme).to eq Gitlab::Themes.default.css_class + end + end + end + + describe '#user_color_scheme' do context 'with a user' do it "returns user's scheme's css_class" do allow(helper).to receive(:current_user) diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb index 49cb7c954b4..a76c75e0c08 100644 --- a/spec/helpers/projects_helper_spec.rb +++ b/spec/helpers/projects_helper_spec.rb @@ -313,23 +313,10 @@ describe ProjectsHelper do it 'returns recent push on the current project' do event = double(:event) - expect(user).to receive(:recent_push).with([project.id]).and_return(event) + expect(user).to receive(:recent_push).with(project).and_return(event) expect(helper.last_push_event).to eq(event) end - - context 'when current user has a fork of the current project' do - let(:fork) { double(:fork, id: 2) } - - it 'returns recent push considering fork events' do - expect(user).to receive(:fork_of).with(project).and_return(fork) - - event_on_fork = double(:event) - expect(user).to receive(:recent_push).with([project.id, fork.id]).and_return(event_on_fork) - - expect(helper.last_push_event).to eq(event_on_fork) - end - end end describe "#project_feature_access_select" do @@ -482,4 +469,15 @@ describe ProjectsHelper do expect(recorder.count).to eq(1) end end + + describe '#git_user_name' do + let(:user) { double(:user, name: 'John "A" Doe53') } + before do + allow(helper).to receive(:current_user).and_return(user) + end + + it 'parses quotes in name' do + expect(helper.send(:git_user_name)).to eq('John \"A\" Doe53') + end + end end diff --git a/spec/initializers/doorkeeper_spec.rb b/spec/initializers/doorkeeper_spec.rb index 74bdbb01166..37cc08b3038 100644 --- a/spec/initializers/doorkeeper_spec.rb +++ b/spec/initializers/doorkeeper_spec.rb @@ -10,7 +10,7 @@ describe Doorkeeper.configuration do describe '#optional_scopes' do it 'matches Gitlab::Auth::OPTIONAL_SCOPES' do - expect(subject.optional_scopes).to eq Gitlab::Auth::OPTIONAL_SCOPES + expect(subject.optional_scopes).to eq Gitlab::Auth::OPTIONAL_SCOPES - Gitlab::Auth::REGISTRY_SCOPES end end diff --git a/spec/javascripts/filtered_search/dropdown_user_spec.js b/spec/javascripts/filtered_search/dropdown_user_spec.js index b3c9bca64cc..02415485d19 100644 --- a/spec/javascripts/filtered_search/dropdown_user_spec.js +++ b/spec/javascripts/filtered_search/dropdown_user_spec.js @@ -10,6 +10,7 @@ describe('Dropdown User', () => { beforeEach(() => { spyOn(gl.DropdownUser.prototype, 'bindEvents').and.callFake(() => {}); spyOn(gl.DropdownUser.prototype, 'getProjectId').and.callFake(() => {}); + spyOn(gl.DropdownUser.prototype, 'getGroupId').and.callFake(() => {}); spyOn(gl.DropdownUtils, 'getSearchInput').and.callFake(() => {}); dropdownUser = new gl.DropdownUser({ @@ -38,6 +39,7 @@ describe('Dropdown User', () => { beforeEach(() => { spyOn(gl.DropdownUser.prototype, 'bindEvents').and.callFake(() => {}); spyOn(gl.DropdownUser.prototype, 'getProjectId').and.callFake(() => {}); + spyOn(gl.DropdownUser.prototype, 'getGroupId').and.callFake(() => {}); }); it('should return endpoint', () => { diff --git a/spec/javascripts/fly_out_nav_spec.js b/spec/javascripts/fly_out_nav_spec.js index f8b37c0edde..f4b4d7980a4 100644 --- a/spec/javascripts/fly_out_nav_spec.js +++ b/spec/javascripts/fly_out_nav_spec.js @@ -271,12 +271,19 @@ describe('Fly out sidebar navigation', () => { }); it('sets transform of sub-items', () => { + const sidebar = document.createElement('div'); const subItems = el.querySelector('.sidebar-sub-level-items'); + + sidebar.style.width = '200px'; + + document.body.appendChild(sidebar); + + setSidebar(sidebar); showSubLevelItems(el); expect( subItems.style.transform, - ).toBe(`translate3d(0px, ${Math.floor(el.getBoundingClientRect().top) - getHeaderHeight()}px, 0px)`); + ).toBe(`translate3d(200px, ${Math.floor(el.getBoundingClientRect().top) - getHeaderHeight()}px, 0px)`); }); it('sets is-above when element is above', () => { diff --git a/spec/javascripts/issue_show/components/app_spec.js b/spec/javascripts/issue_show/components/app_spec.js index 39065814bc2..583a3a74d77 100644 --- a/spec/javascripts/issue_show/components/app_spec.js +++ b/spec/javascripts/issue_show/components/app_spec.js @@ -42,7 +42,6 @@ describe('Issuable output', () => { initialDescriptionText: '', markdownPreviewPath: '/', markdownDocsPath: '/', - isConfidential: false, projectNamespace: '/', projectPath: '/', }, @@ -157,30 +156,6 @@ describe('Issuable output', () => { }); }); - it('reloads the page if the confidential status has changed', (done) => { - spyOn(gl.utils, 'visitUrl'); - spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve) => { - resolve({ - json() { - return { - confidential: true, - web_url: location.pathname, - }; - }, - }); - })); - - vm.updateIssuable(); - - setTimeout(() => { - expect( - gl.utils.visitUrl, - ).toHaveBeenCalledWith(location.pathname); - - done(); - }); - }); - it('correctly updates issuable data', (done) => { spyOn(vm.service, 'updateIssuable').and.callFake(() => new Promise((resolve) => { resolve(); diff --git a/spec/lib/banzai/pipeline/email_pipeline_spec.rb b/spec/lib/banzai/pipeline/email_pipeline_spec.rb new file mode 100644 index 00000000000..6a11ca2f9d5 --- /dev/null +++ b/spec/lib/banzai/pipeline/email_pipeline_spec.rb @@ -0,0 +1,14 @@ +require 'rails_helper' + +describe Banzai::Pipeline::EmailPipeline do + describe '.filters' do + it 'returns the expected type' do + expect(described_class.filters).to be_kind_of(Banzai::FilterArray) + end + + it 'excludes ImageLazyLoadFilter' do + expect(described_class.filters).not_to be_empty + expect(described_class.filters).not_to include(Banzai::Filter::ImageLazyLoadFilter) + end + end +end diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb deleted file mode 100644 index 1efd3113a43..00000000000 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ /dev/null @@ -1,1697 +0,0 @@ -require 'spec_helper' - -module Ci - describe GitlabCiYamlProcessor, :lib do - subject { described_class.new(config, path) } - let(:path) { 'path' } - - describe 'our current .gitlab-ci.yml' do - let(:config) { File.read("#{Rails.root}/.gitlab-ci.yml") } - - it 'is valid' do - error_message = described_class.validation_message(config) - - expect(error_message).to be_nil - end - end - - describe '#build_attributes' do - subject { described_class.new(config, path).build_attributes(:rspec) } - - describe 'coverage entry' do - describe 'code coverage regexp' do - let(:config) do - YAML.dump(rspec: { script: 'rspec', - coverage: '/Code coverage: \d+\.\d+/' }) - end - - it 'includes coverage regexp in build attributes' do - expect(subject) - .to include(coverage_regex: 'Code coverage: \d+\.\d+') - end - end - end - - describe 'retry entry' do - context 'when retry count is specified' do - let(:config) do - YAML.dump(rspec: { script: 'rspec', retry: 1 }) - end - - it 'includes retry count in build options attribute' do - expect(subject[:options]).to include(retry: 1) - end - end - - context 'when retry count is not specified' do - let(:config) do - YAML.dump(rspec: { script: 'rspec' }) - end - - it 'does not persist retry count in the database' do - expect(subject[:options]).not_to have_key(:retry) - end - end - end - - describe 'allow failure entry' do - context 'when job is a manual action' do - context 'when allow_failure is defined' do - let(:config) do - YAML.dump(rspec: { script: 'rspec', - when: 'manual', - allow_failure: false }) - end - - it 'is not allowed to fail' do - expect(subject[:allow_failure]).to be false - end - end - - context 'when allow_failure is not defined' do - let(:config) do - YAML.dump(rspec: { script: 'rspec', - when: 'manual' }) - end - - it 'is allowed to fail' do - expect(subject[:allow_failure]).to be true - end - end - end - - context 'when job is not a manual action' do - context 'when allow_failure is defined' do - let(:config) do - YAML.dump(rspec: { script: 'rspec', - allow_failure: false }) - end - - it 'is not allowed to fail' do - expect(subject[:allow_failure]).to be false - end - end - - context 'when allow_failure is not defined' do - let(:config) do - YAML.dump(rspec: { script: 'rspec' }) - end - - it 'is not allowed to fail' do - expect(subject[:allow_failure]).to be false - end - end - end - end - end - - describe '#stage_seeds' do - context 'when no refs policy is specified' do - let(:config) do - YAML.dump(production: { stage: 'deploy', script: 'cap prod' }, - rspec: { stage: 'test', script: 'rspec' }, - spinach: { stage: 'test', script: 'spinach' }) - end - - let(:pipeline) { create(:ci_empty_pipeline) } - - it 'correctly fabricates a stage seeds object' do - seeds = subject.stage_seeds(pipeline) - - expect(seeds.size).to eq 2 - expect(seeds.first.stage[:name]).to eq 'test' - expect(seeds.second.stage[:name]).to eq 'deploy' - expect(seeds.first.builds.dig(0, :name)).to eq 'rspec' - expect(seeds.first.builds.dig(1, :name)).to eq 'spinach' - expect(seeds.second.builds.dig(0, :name)).to eq 'production' - end - end - - context 'when refs policy is specified' do - let(:config) do - YAML.dump(production: { stage: 'deploy', script: 'cap prod', only: ['master'] }, - spinach: { stage: 'test', script: 'spinach', only: ['tags'] }) - end - - let(:pipeline) do - create(:ci_empty_pipeline, ref: 'feature', tag: true) - end - - it 'returns stage seeds only assigned to master to master' do - seeds = subject.stage_seeds(pipeline) - - expect(seeds.size).to eq 1 - expect(seeds.first.stage[:name]).to eq 'test' - expect(seeds.first.builds.dig(0, :name)).to eq 'spinach' - end - end - - context 'when source policy is specified' do - let(:config) do - YAML.dump(production: { stage: 'deploy', script: 'cap prod', only: ['triggers'] }, - spinach: { stage: 'test', script: 'spinach', only: ['schedules'] }) - end - - let(:pipeline) do - create(:ci_empty_pipeline, source: :schedule) - end - - it 'returns stage seeds only assigned to schedules' do - seeds = subject.stage_seeds(pipeline) - - expect(seeds.size).to eq 1 - expect(seeds.first.stage[:name]).to eq 'test' - expect(seeds.first.builds.dig(0, :name)).to eq 'spinach' - end - end - - context 'when kubernetes policy is specified' do - let(:pipeline) { create(:ci_empty_pipeline) } - - let(:config) do - YAML.dump( - spinach: { stage: 'test', script: 'spinach' }, - production: { - stage: 'deploy', - script: 'cap', - only: { kubernetes: 'active' } - } - ) - end - - context 'when kubernetes is active' do - let(:project) { create(:kubernetes_project) } - let(:pipeline) { create(:ci_empty_pipeline, project: project) } - - it 'returns seeds for kubernetes dependent job' do - seeds = subject.stage_seeds(pipeline) - - expect(seeds.size).to eq 2 - expect(seeds.first.builds.dig(0, :name)).to eq 'spinach' - expect(seeds.second.builds.dig(0, :name)).to eq 'production' - end - end - - context 'when kubernetes is not active' do - it 'does not return seeds for kubernetes dependent job' do - seeds = subject.stage_seeds(pipeline) - - expect(seeds.size).to eq 1 - expect(seeds.first.builds.dig(0, :name)).to eq 'spinach' - end - end - end - end - - describe "#builds_for_stage_and_ref" do - let(:type) { 'test' } - - it "returns builds if no branch specified" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec" } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(1) - expect(config_processor.builds_for_stage_and_ref(type, "master").first).to eq({ - stage: "test", - stage_idx: 1, - name: "rspec", - commands: "pwd\nrspec", - coverage_regex: nil, - tag_list: [], - options: { - before_script: ["pwd"], - script: ["rspec"] - }, - allow_failure: false, - when: "on_success", - environment: nil, - yaml_variables: [] - }) - end - - describe 'only' do - it "does not return builds if only has another branch" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", only: ["deploy"] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(0) - end - - it "does not return builds if only has regexp with another branch" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", only: ["/^deploy$/"] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(0) - end - - it "returns builds if only has specified this branch" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", only: ["master"] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(1) - end - - it "returns builds if only has a list of branches including specified" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", type: type, only: %w(master deploy) } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1) - end - - it "returns builds if only has a branches keyword specified" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", type: type, only: ["branches"] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1) - end - - it "does not return builds if only has a tags keyword" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", type: type, only: ["tags"] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0) - end - - it "returns builds if only has special keywords specified and source matches" do - possibilities = [{ keyword: 'pushes', source: 'push' }, - { keyword: 'web', source: 'web' }, - { keyword: 'triggers', source: 'trigger' }, - { keyword: 'schedules', source: 'schedule' }, - { keyword: 'api', source: 'api' }, - { keyword: 'external', source: 'external' }] - - possibilities.each do |possibility| - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", type: type, only: [possibility[:keyword]] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "deploy", false, possibility[:source]).size).to eq(1) - end - end - - it "does not return builds if only has special keywords specified and source doesn't match" do - possibilities = [{ keyword: 'pushes', source: 'web' }, - { keyword: 'web', source: 'push' }, - { keyword: 'triggers', source: 'schedule' }, - { keyword: 'schedules', source: 'external' }, - { keyword: 'api', source: 'trigger' }, - { keyword: 'external', source: 'api' }] - - possibilities.each do |possibility| - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", type: type, only: [possibility[:keyword]] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "deploy", false, possibility[:source]).size).to eq(0) - end - end - - it "returns builds if only has current repository path" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", type: type, only: ["branches@path"] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1) - end - - it "does not return builds if only has different repository path" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", type: type, only: ["branches@fork"] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0) - end - - it "returns build only for specified type" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", type: "test", only: %w(master deploy) }, - staging: { script: "deploy", type: "deploy", only: %w(master deploy) }, - production: { script: "deploy", type: "deploy", only: ["master@path", "deploy"] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, 'fork') - - expect(config_processor.builds_for_stage_and_ref("deploy", "deploy").size).to eq(2) - expect(config_processor.builds_for_stage_and_ref("test", "deploy").size).to eq(1) - expect(config_processor.builds_for_stage_and_ref("deploy", "master").size).to eq(1) - end - - context 'for invalid value' do - let(:config) { { rspec: { script: "rspec", type: "test", only: only } } } - let(:processor) { GitlabCiYamlProcessor.new(YAML.dump(config)) } - - context 'when it is integer' do - let(:only) { 1 } - - it do - expect { processor }.to raise_error(GitlabCiYamlProcessor::ValidationError, - 'jobs:rspec:only has to be either an array of conditions or a hash') - end - end - - context 'when it is an array of integers' do - let(:only) { [1, 1] } - - it do - expect { processor }.to raise_error(GitlabCiYamlProcessor::ValidationError, - 'jobs:rspec:only config should be an array of strings or regexps') - end - end - - context 'when it is invalid regex' do - let(:only) { ["/*invalid/"] } - - it do - expect { processor }.to raise_error(GitlabCiYamlProcessor::ValidationError, - 'jobs:rspec:only config should be an array of strings or regexps') - end - end - end - end - - describe 'except' do - it "returns builds if except has another branch" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", except: ["deploy"] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(1) - end - - it "returns builds if except has regexp with another branch" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", except: ["/^deploy$/"] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(1) - end - - it "does not return builds if except has specified this branch" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", except: ["master"] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(0) - end - - it "does not return builds if except has a list of branches including specified" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", type: type, except: %w(master deploy) } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0) - end - - it "does not return builds if except has a branches keyword specified" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", type: type, except: ["branches"] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0) - end - - it "returns builds if except has a tags keyword" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", type: type, except: ["tags"] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1) - end - - it "does not return builds if except has special keywords specified and source matches" do - possibilities = [{ keyword: 'pushes', source: 'push' }, - { keyword: 'web', source: 'web' }, - { keyword: 'triggers', source: 'trigger' }, - { keyword: 'schedules', source: 'schedule' }, - { keyword: 'api', source: 'api' }, - { keyword: 'external', source: 'external' }] - - possibilities.each do |possibility| - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", type: type, except: [possibility[:keyword]] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "deploy", false, possibility[:source]).size).to eq(0) - end - end - - it "returns builds if except has special keywords specified and source doesn't match" do - possibilities = [{ keyword: 'pushes', source: 'web' }, - { keyword: 'web', source: 'push' }, - { keyword: 'triggers', source: 'schedule' }, - { keyword: 'schedules', source: 'external' }, - { keyword: 'api', source: 'trigger' }, - { keyword: 'external', source: 'api' }] - - possibilities.each do |possibility| - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", type: type, except: [possibility[:keyword]] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "deploy", false, possibility[:source]).size).to eq(1) - end - end - - it "does not return builds if except has current repository path" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", type: type, except: ["branches@path"] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0) - end - - it "returns builds if except has different repository path" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", type: type, except: ["branches@fork"] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1) - end - - it "returns build except specified type" do - config = YAML.dump({ - before_script: ["pwd"], - rspec: { script: "rspec", type: "test", except: ["master", "deploy", "test@fork"] }, - staging: { script: "deploy", type: "deploy", except: ["master"] }, - production: { script: "deploy", type: "deploy", except: ["master@fork"] } - }) - - config_processor = GitlabCiYamlProcessor.new(config, 'fork') - - expect(config_processor.builds_for_stage_and_ref("deploy", "deploy").size).to eq(2) - expect(config_processor.builds_for_stage_and_ref("test", "test").size).to eq(0) - expect(config_processor.builds_for_stage_and_ref("deploy", "master").size).to eq(0) - end - - context 'for invalid value' do - let(:config) { { rspec: { script: "rspec", except: except } } } - let(:processor) { GitlabCiYamlProcessor.new(YAML.dump(config)) } - - context 'when it is integer' do - let(:except) { 1 } - - it do - expect { processor }.to raise_error(GitlabCiYamlProcessor::ValidationError, - 'jobs:rspec:except has to be either an array of conditions or a hash') - end - end - - context 'when it is an array of integers' do - let(:except) { [1, 1] } - - it do - expect { processor }.to raise_error(GitlabCiYamlProcessor::ValidationError, - 'jobs:rspec:except config should be an array of strings or regexps') - end - end - - context 'when it is invalid regex' do - let(:except) { ["/*invalid/"] } - - it do - expect { processor }.to raise_error(GitlabCiYamlProcessor::ValidationError, - 'jobs:rspec:except config should be an array of strings or regexps') - end - end - end - end - end - - describe "Scripts handling" do - let(:config_data) { YAML.dump(config) } - let(:config_processor) { GitlabCiYamlProcessor.new(config_data, path) } - - subject { config_processor.builds_for_stage_and_ref("test", "master").first } - - describe "before_script" do - context "in global context" do - let(:config) do - { - before_script: ["global script"], - test: { script: ["script"] } - } - end - - it "return commands with scripts concencaced" do - expect(subject[:commands]).to eq("global script\nscript") - end - end - - context "overwritten in local context" do - let(:config) do - { - before_script: ["global script"], - test: { before_script: ["local script"], script: ["script"] } - } - end - - it "return commands with scripts concencaced" do - expect(subject[:commands]).to eq("local script\nscript") - end - end - end - - describe "script" do - let(:config) do - { - test: { script: ["script"] } - } - end - - it "return commands with scripts concencaced" do - expect(subject[:commands]).to eq("script") - end - end - - describe "after_script" do - context "in global context" do - let(:config) do - { - after_script: ["after_script"], - test: { script: ["script"] } - } - end - - it "return after_script in options" do - expect(subject[:options][:after_script]).to eq(["after_script"]) - end - end - - context "overwritten in local context" do - let(:config) do - { - after_script: ["local after_script"], - test: { after_script: ["local after_script"], script: ["script"] } - } - end - - it "return after_script in options" do - expect(subject[:options][:after_script]).to eq(["local after_script"]) - end - end - end - end - - describe "Image and service handling" do - context "when extended docker configuration is used" do - it "returns image and service when defined" do - config = YAML.dump({ image: { name: "ruby:2.1", entrypoint: ["/usr/local/bin/init", "run"] }, - services: ["mysql", { name: "docker:dind", alias: "docker", - entrypoint: ["/usr/local/bin/init", "run"], - command: ["/usr/local/bin/init", "run"] }], - before_script: ["pwd"], - rspec: { script: "rspec" } }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) - expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({ - stage: "test", - stage_idx: 1, - name: "rspec", - commands: "pwd\nrspec", - coverage_regex: nil, - tag_list: [], - options: { - before_script: ["pwd"], - script: ["rspec"], - image: { name: "ruby:2.1", entrypoint: ["/usr/local/bin/init", "run"] }, - services: [{ name: "mysql" }, - { name: "docker:dind", alias: "docker", entrypoint: ["/usr/local/bin/init", "run"], - command: ["/usr/local/bin/init", "run"] }] - }, - allow_failure: false, - when: "on_success", - environment: nil, - yaml_variables: [] - }) - end - - it "returns image and service when overridden for job" do - config = YAML.dump({ image: "ruby:2.1", - services: ["mysql"], - before_script: ["pwd"], - rspec: { image: { name: "ruby:2.5", entrypoint: ["/usr/local/bin/init", "run"] }, - services: [{ name: "postgresql", alias: "db-pg", - entrypoint: ["/usr/local/bin/init", "run"], - command: ["/usr/local/bin/init", "run"] }, "docker:dind"], - script: "rspec" } }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) - expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({ - stage: "test", - stage_idx: 1, - name: "rspec", - commands: "pwd\nrspec", - coverage_regex: nil, - tag_list: [], - options: { - before_script: ["pwd"], - script: ["rspec"], - image: { name: "ruby:2.5", entrypoint: ["/usr/local/bin/init", "run"] }, - services: [{ name: "postgresql", alias: "db-pg", entrypoint: ["/usr/local/bin/init", "run"], - command: ["/usr/local/bin/init", "run"] }, - { name: "docker:dind" }] - }, - allow_failure: false, - when: "on_success", - environment: nil, - yaml_variables: [] - }) - end - end - - context "when etended docker configuration is not used" do - it "returns image and service when defined" do - config = YAML.dump({ image: "ruby:2.1", - services: ["mysql", "docker:dind"], - before_script: ["pwd"], - rspec: { script: "rspec" } }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) - expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({ - stage: "test", - stage_idx: 1, - name: "rspec", - commands: "pwd\nrspec", - coverage_regex: nil, - tag_list: [], - options: { - before_script: ["pwd"], - script: ["rspec"], - image: { name: "ruby:2.1" }, - services: [{ name: "mysql" }, { name: "docker:dind" }] - }, - allow_failure: false, - when: "on_success", - environment: nil, - yaml_variables: [] - }) - end - - it "returns image and service when overridden for job" do - config = YAML.dump({ image: "ruby:2.1", - services: ["mysql"], - before_script: ["pwd"], - rspec: { image: "ruby:2.5", services: ["postgresql", "docker:dind"], script: "rspec" } }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) - expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({ - stage: "test", - stage_idx: 1, - name: "rspec", - commands: "pwd\nrspec", - coverage_regex: nil, - tag_list: [], - options: { - before_script: ["pwd"], - script: ["rspec"], - image: { name: "ruby:2.5" }, - services: [{ name: "postgresql" }, { name: "docker:dind" }] - }, - allow_failure: false, - when: "on_success", - environment: nil, - yaml_variables: [] - }) - end - end - end - - describe 'Variables' do - let(:config_processor) { GitlabCiYamlProcessor.new(YAML.dump(config), path) } - - subject { config_processor.builds.first[:yaml_variables] } - - context 'when global variables are defined' do - let(:variables) do - { 'VAR1' => 'value1', 'VAR2' => 'value2' } - end - let(:config) do - { - variables: variables, - before_script: ['pwd'], - rspec: { script: 'rspec' } - } - end - - it 'returns global variables' do - expect(subject).to contain_exactly( - { key: 'VAR1', value: 'value1', public: true }, - { key: 'VAR2', value: 'value2', public: true } - ) - end - end - - context 'when job and global variables are defined' do - let(:global_variables) do - { 'VAR1' => 'global1', 'VAR3' => 'global3' } - end - let(:job_variables) do - { 'VAR1' => 'value1', 'VAR2' => 'value2' } - end - let(:config) do - { - before_script: ['pwd'], - variables: global_variables, - rspec: { script: 'rspec', variables: job_variables } - } - end - - it 'returns all unique variables' do - expect(subject).to contain_exactly( - { key: 'VAR3', value: 'global3', public: true }, - { key: 'VAR1', value: 'value1', public: true }, - { key: 'VAR2', value: 'value2', public: true } - ) - end - end - - context 'when job variables are defined' do - let(:config) do - { - before_script: ['pwd'], - rspec: { script: 'rspec', variables: variables } - } - end - - context 'when syntax is correct' do - let(:variables) do - { 'VAR1' => 'value1', 'VAR2' => 'value2' } - end - - it 'returns job variables' do - expect(subject).to contain_exactly( - { key: 'VAR1', value: 'value1', public: true }, - { key: 'VAR2', value: 'value2', public: true } - ) - end - end - - context 'when syntax is incorrect' do - context 'when variables defined but invalid' do - let(:variables) do - %w(VAR1 value1 VAR2 value2) - end - - it 'raises error' do - expect { subject } - .to raise_error(GitlabCiYamlProcessor::ValidationError, - /jobs:rspec:variables config should be a hash of key value pairs/) - end - end - - context 'when variables key defined but value not specified' do - let(:variables) do - nil - end - - it 'returns empty array' do - ## - # When variables config is empty, we assume this is a valid - # configuration, see issue #18775 - # - expect(subject).to be_an_instance_of(Array) - expect(subject).to be_empty - end - end - end - end - - context 'when job variables are not defined' do - let(:config) do - { - before_script: ['pwd'], - rspec: { script: 'rspec' } - } - end - - it 'returns empty array' do - expect(subject).to be_an_instance_of(Array) - expect(subject).to be_empty - end - end - end - - describe "When" do - %w(on_success on_failure always).each do |when_state| - it "returns #{when_state} when defined" do - config = YAML.dump({ - rspec: { script: "rspec", when: when_state } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - builds = config_processor.builds_for_stage_and_ref("test", "master") - expect(builds.size).to eq(1) - expect(builds.first[:when]).to eq(when_state) - end - end - end - - describe 'cache' do - context 'when cache definition has unknown keys' do - it 'raises relevant validation error' do - config = YAML.dump( - { cache: { untracked: true, invalid: 'key' }, - rspec: { script: 'rspec' } }) - - expect { GitlabCiYamlProcessor.new(config) }.to raise_error( - GitlabCiYamlProcessor::ValidationError, - 'cache config contains unknown keys: invalid' - ) - end - end - - it "returns cache when defined globally" do - config = YAML.dump({ - cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'key' }, - rspec: { - script: "rspec" - } - }) - - config_processor = GitlabCiYamlProcessor.new(config) - - expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) - expect(config_processor.builds_for_stage_and_ref("test", "master").first[:options][:cache]).to eq( - paths: ["logs/", "binaries/"], - untracked: true, - key: 'key', - policy: 'pull-push' - ) - end - - it "returns cache when defined in a job" do - config = YAML.dump({ - rspec: { - cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'key' }, - script: "rspec" - } - }) - - config_processor = GitlabCiYamlProcessor.new(config) - - expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) - expect(config_processor.builds_for_stage_and_ref("test", "master").first[:options][:cache]).to eq( - paths: ["logs/", "binaries/"], - untracked: true, - key: 'key', - policy: 'pull-push' - ) - end - - it "overwrite cache when defined for a job and globally" do - config = YAML.dump({ - cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'global' }, - rspec: { - script: "rspec", - cache: { paths: ["test/"], untracked: false, key: 'local' } - } - }) - - config_processor = GitlabCiYamlProcessor.new(config) - - expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) - expect(config_processor.builds_for_stage_and_ref("test", "master").first[:options][:cache]).to eq( - paths: ["test/"], - untracked: false, - key: 'local', - policy: 'pull-push' - ) - end - end - - describe "Artifacts" do - it "returns artifacts when defined" do - config = YAML.dump({ - image: "ruby:2.1", - services: ["mysql"], - before_script: ["pwd"], - rspec: { - artifacts: { - paths: ["logs/", "binaries/"], - untracked: true, - name: "custom_name", - expire_in: "7d" - }, - script: "rspec" - } - }) - - config_processor = GitlabCiYamlProcessor.new(config) - - expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) - expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({ - stage: "test", - stage_idx: 1, - name: "rspec", - commands: "pwd\nrspec", - coverage_regex: nil, - tag_list: [], - options: { - before_script: ["pwd"], - script: ["rspec"], - image: { name: "ruby:2.1" }, - services: [{ name: "mysql" }], - artifacts: { - name: "custom_name", - paths: ["logs/", "binaries/"], - untracked: true, - expire_in: "7d" - } - }, - when: "on_success", - allow_failure: false, - environment: nil, - yaml_variables: [] - }) - end - - %w[on_success on_failure always].each do |when_state| - it "returns artifacts for when #{when_state} defined" do - config = YAML.dump({ - rspec: { - script: "rspec", - artifacts: { paths: ["logs/", "binaries/"], when: when_state } - } - }) - - config_processor = GitlabCiYamlProcessor.new(config, path) - - builds = config_processor.builds_for_stage_and_ref("test", "master") - expect(builds.size).to eq(1) - expect(builds.first[:options][:artifacts][:when]).to eq(when_state) - end - end - end - - describe '#environment' do - let(:config) do - { - deploy_to_production: { stage: 'deploy', script: 'test', environment: environment } - } - end - - let(:processor) { GitlabCiYamlProcessor.new(YAML.dump(config)) } - let(:builds) { processor.builds_for_stage_and_ref('deploy', 'master') } - - context 'when a production environment is specified' do - let(:environment) { 'production' } - - it 'does return production' do - expect(builds.size).to eq(1) - expect(builds.first[:environment]).to eq(environment) - expect(builds.first[:options]).to include(environment: { name: environment, action: "start" }) - end - end - - context 'when hash is specified' do - let(:environment) do - { name: 'production', - url: 'http://production.gitlab.com' } - end - - it 'does return production and URL' do - expect(builds.size).to eq(1) - expect(builds.first[:environment]).to eq(environment[:name]) - expect(builds.first[:options]).to include(environment: environment) - end - - context 'the url has a port as variable' do - let(:environment) do - { name: 'production', - url: 'http://production.gitlab.com:$PORT' } - end - - it 'allows a variable for the port' do - expect(builds.size).to eq(1) - expect(builds.first[:environment]).to eq(environment[:name]) - expect(builds.first[:options]).to include(environment: environment) - end - end - end - - context 'when no environment is specified' do - let(:environment) { nil } - - it 'does return nil environment' do - expect(builds.size).to eq(1) - expect(builds.first[:environment]).to be_nil - end - end - - context 'is not a string' do - let(:environment) { 1 } - - it 'raises error' do - expect { builds }.to raise_error( - 'jobs:deploy_to_production:environment config should be a hash or a string') - end - end - - context 'is not a valid string' do - let(:environment) { 'production:staging' } - - it 'raises error' do - expect { builds }.to raise_error("jobs:deploy_to_production:environment name #{Gitlab::Regex.environment_name_regex_message}") - end - end - - context 'when on_stop is specified' do - let(:review) { { stage: 'deploy', script: 'test', environment: { name: 'review', on_stop: 'close_review' } } } - let(:config) { { review: review, close_review: close_review }.compact } - - context 'with matching job' do - let(:close_review) { { stage: 'deploy', script: 'test', environment: { name: 'review', action: 'stop' } } } - - it 'does return a list of builds' do - expect(builds.size).to eq(2) - expect(builds.first[:environment]).to eq('review') - end - end - - context 'without matching job' do - let(:close_review) { nil } - - it 'raises error' do - expect { builds }.to raise_error('review job: on_stop job close_review is not defined') - end - end - - context 'with close job without environment' do - let(:close_review) { { stage: 'deploy', script: 'test' } } - - it 'raises error' do - expect { builds }.to raise_error('review job: on_stop job close_review does not have environment defined') - end - end - - context 'with close job for different environment' do - let(:close_review) { { stage: 'deploy', script: 'test', environment: 'production' } } - - it 'raises error' do - expect { builds }.to raise_error('review job: on_stop job close_review have different environment name') - end - end - - context 'with close job without stop action' do - let(:close_review) { { stage: 'deploy', script: 'test', environment: { name: 'review' } } } - - it 'raises error' do - expect { builds }.to raise_error('review job: on_stop job close_review needs to have action stop defined') - end - end - end - end - - describe "Dependencies" do - let(:config) do - { - build1: { stage: 'build', script: 'test' }, - build2: { stage: 'build', script: 'test' }, - test1: { stage: 'test', script: 'test', dependencies: dependencies }, - test2: { stage: 'test', script: 'test' }, - deploy: { stage: 'test', script: 'test' } - } - end - - subject { GitlabCiYamlProcessor.new(YAML.dump(config)) } - - context 'no dependencies' do - let(:dependencies) { } - - it { expect { subject }.not_to raise_error } - end - - context 'dependencies to builds' do - let(:dependencies) { %w(build1 build2) } - - it { expect { subject }.not_to raise_error } - end - - context 'dependencies to builds defined as symbols' do - let(:dependencies) { [:build1, :build2] } - - it { expect { subject }.not_to raise_error } - end - - context 'undefined dependency' do - let(:dependencies) { ['undefined'] } - - it { expect { subject }.to raise_error(GitlabCiYamlProcessor::ValidationError, 'test1 job: undefined dependency: undefined') } - end - - context 'dependencies to deploy' do - let(:dependencies) { ['deploy'] } - - it { expect { subject }.to raise_error(GitlabCiYamlProcessor::ValidationError, 'test1 job: dependency deploy is not defined in prior stages') } - end - end - - describe "Hidden jobs" do - let(:config_processor) { GitlabCiYamlProcessor.new(config) } - subject { config_processor.builds_for_stage_and_ref("test", "master") } - - shared_examples 'hidden_job_handling' do - it "doesn't create jobs that start with dot" do - expect(subject.size).to eq(1) - expect(subject.first).to eq({ - stage: "test", - stage_idx: 1, - name: "normal_job", - commands: "test", - coverage_regex: nil, - tag_list: [], - options: { - script: ["test"] - }, - when: "on_success", - allow_failure: false, - environment: nil, - yaml_variables: [] - }) - end - end - - context 'when hidden job have a script definition' do - let(:config) do - YAML.dump({ - '.hidden_job' => { image: 'ruby:2.1', script: 'test' }, - 'normal_job' => { script: 'test' } - }) - end - - it_behaves_like 'hidden_job_handling' - end - - context "when hidden job doesn't have a script definition" do - let(:config) do - YAML.dump({ - '.hidden_job' => { image: 'ruby:2.1' }, - 'normal_job' => { script: 'test' } - }) - end - - it_behaves_like 'hidden_job_handling' - end - end - - describe "YAML Alias/Anchor" do - let(:config_processor) { GitlabCiYamlProcessor.new(config) } - subject { config_processor.builds_for_stage_and_ref("build", "master") } - - shared_examples 'job_templates_handling' do - it "is correctly supported for jobs" do - expect(subject.size).to eq(2) - expect(subject.first).to eq({ - stage: "build", - stage_idx: 0, - name: "job1", - commands: "execute-script-for-job", - coverage_regex: nil, - tag_list: [], - options: { - script: ["execute-script-for-job"] - }, - when: "on_success", - allow_failure: false, - environment: nil, - yaml_variables: [] - }) - expect(subject.second).to eq({ - stage: "build", - stage_idx: 0, - name: "job2", - commands: "execute-script-for-job", - coverage_regex: nil, - tag_list: [], - options: { - script: ["execute-script-for-job"] - }, - when: "on_success", - allow_failure: false, - environment: nil, - yaml_variables: [] - }) - end - end - - context 'when template is a job' do - let(:config) do - <<EOT -job1: &JOBTMPL - stage: build - script: execute-script-for-job - -job2: *JOBTMPL -EOT - end - - it_behaves_like 'job_templates_handling' - end - - context 'when template is a hidden job' do - let(:config) do - <<EOT -.template: &JOBTMPL - stage: build - script: execute-script-for-job - -job1: *JOBTMPL - -job2: *JOBTMPL -EOT - end - - it_behaves_like 'job_templates_handling' - end - - context 'when job adds its own keys to a template definition' do - let(:config) do - <<EOT -.template: &JOBTMPL - stage: build - -job1: - <<: *JOBTMPL - script: execute-script-for-job - -job2: - <<: *JOBTMPL - script: execute-script-for-job -EOT - end - - it_behaves_like 'job_templates_handling' - end - end - - describe "Error handling" do - it "fails to parse YAML" do - expect {GitlabCiYamlProcessor.new("invalid: yaml: test")}.to raise_error(Psych::SyntaxError) - end - - it "indicates that object is invalid" do - expect {GitlabCiYamlProcessor.new("invalid_yaml")}.to raise_error(GitlabCiYamlProcessor::ValidationError) - end - - it "returns errors if tags parameter is invalid" do - config = YAML.dump({ rspec: { script: "test", tags: "mysql" } }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec tags should be an array of strings") - end - - it "returns errors if before_script parameter is invalid" do - config = YAML.dump({ before_script: "bundle update", rspec: { script: "test" } }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "before_script config should be an array of strings") - end - - it "returns errors if job before_script parameter is not an array of strings" do - config = YAML.dump({ rspec: { script: "test", before_script: [10, "test"] } }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:before_script config should be an array of strings") - end - - it "returns errors if after_script parameter is invalid" do - config = YAML.dump({ after_script: "bundle update", rspec: { script: "test" } }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "after_script config should be an array of strings") - end - - it "returns errors if job after_script parameter is not an array of strings" do - config = YAML.dump({ rspec: { script: "test", after_script: [10, "test"] } }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:after_script config should be an array of strings") - end - - it "returns errors if image parameter is invalid" do - config = YAML.dump({ image: ["test"], rspec: { script: "test" } }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "image config should be a hash or a string") - end - - it "returns errors if job name is blank" do - config = YAML.dump({ '' => { script: "test" } }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:job name can't be blank") - end - - it "returns errors if job name is non-string" do - config = YAML.dump({ 10 => { script: "test" } }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:10 name should be a symbol") - end - - it "returns errors if job image parameter is invalid" do - config = YAML.dump({ rspec: { script: "test", image: ["test"] } }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:image config should be a hash or a string") - end - - it "returns errors if services parameter is not an array" do - config = YAML.dump({ services: "test", rspec: { script: "test" } }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "services config should be a array") - end - - it "returns errors if services parameter is not an array of strings" do - config = YAML.dump({ services: [10, "test"], rspec: { script: "test" } }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "service config should be a hash or a string") - end - - it "returns errors if job services parameter is not an array" do - config = YAML.dump({ rspec: { script: "test", services: "test" } }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:services config should be a array") - end - - it "returns errors if job services parameter is not an array of strings" do - config = YAML.dump({ rspec: { script: "test", services: [10, "test"] } }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "service config should be a hash or a string") - end - - it "returns error if job configuration is invalid" do - config = YAML.dump({ extra: "bundle update" }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:extra config should be a hash") - end - - it "returns errors if services configuration is not correct" do - config = YAML.dump({ extra: { script: 'rspec', services: "test" } }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:extra:services config should be a array") - end - - it "returns errors if there are no jobs defined" do - config = YAML.dump({ before_script: ["bundle update"] }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs config should contain at least one visible job") - end - - it "returns errors if there are no visible jobs defined" do - config = YAML.dump({ before_script: ["bundle update"], '.hidden'.to_sym => { script: 'ls' } }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs config should contain at least one visible job") - end - - it "returns errors if job allow_failure parameter is not an boolean" do - config = YAML.dump({ rspec: { script: "test", allow_failure: "string" } }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec allow failure should be a boolean value") - end - - it "returns errors if job stage is not a string" do - config = YAML.dump({ rspec: { script: "test", type: 1 } }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:type config should be a string") - end - - it "returns errors if job stage is not a pre-defined stage" do - config = YAML.dump({ rspec: { script: "test", type: "acceptance" } }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: stage parameter should be build, test, deploy") - end - - it "returns errors if job stage is not a defined stage" do - config = YAML.dump({ types: %w(build test), rspec: { script: "test", type: "acceptance" } }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: stage parameter should be build, test") - end - - it "returns errors if stages is not an array" do - config = YAML.dump({ stages: "test", rspec: { script: "test" } }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "stages config should be an array of strings") - end - - it "returns errors if stages is not an array of strings" do - config = YAML.dump({ stages: [true, "test"], rspec: { script: "test" } }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "stages config should be an array of strings") - end - - it "returns errors if variables is not a map" do - config = YAML.dump({ variables: "test", rspec: { script: "test" } }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "variables config should be a hash of key value pairs") - end - - it "returns errors if variables is not a map of key-value strings" do - config = YAML.dump({ variables: { test: false }, rspec: { script: "test" } }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "variables config should be a hash of key value pairs") - end - - it "returns errors if job when is not on_success, on_failure or always" do - config = YAML.dump({ rspec: { script: "test", when: 1 } }) - expect do - GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec when should be on_success, on_failure, always or manual") - end - - it "returns errors if job artifacts:name is not an a string" do - config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { name: 1 } } }) - expect do - GitlabCiYamlProcessor.new(config) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts name should be a string") - end - - it "returns errors if job artifacts:when is not an a predefined value" do - config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { when: 1 } } }) - expect do - GitlabCiYamlProcessor.new(config) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts when should be on_success, on_failure or always") - end - - it "returns errors if job artifacts:expire_in is not an a string" do - config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { expire_in: 1 } } }) - expect do - GitlabCiYamlProcessor.new(config) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts expire in should be a duration") - end - - it "returns errors if job artifacts:expire_in is not an a valid duration" do - config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { expire_in: "7 elephants" } } }) - expect do - GitlabCiYamlProcessor.new(config) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts expire in should be a duration") - end - - it "returns errors if job artifacts:untracked is not an array of strings" do - config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { untracked: "string" } } }) - expect do - GitlabCiYamlProcessor.new(config) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts untracked should be a boolean value") - end - - it "returns errors if job artifacts:paths is not an array of strings" do - config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { paths: "string" } } }) - expect do - GitlabCiYamlProcessor.new(config) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts paths should be an array of strings") - end - - it "returns errors if cache:untracked is not an array of strings" do - config = YAML.dump({ cache: { untracked: "string" }, rspec: { script: "test" } }) - expect do - GitlabCiYamlProcessor.new(config) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "cache:untracked config should be a boolean value") - end - - it "returns errors if cache:paths is not an array of strings" do - config = YAML.dump({ cache: { paths: "string" }, rspec: { script: "test" } }) - expect do - GitlabCiYamlProcessor.new(config) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "cache:paths config should be an array of strings") - end - - it "returns errors if cache:key is not a string" do - config = YAML.dump({ cache: { key: 1 }, rspec: { script: "test" } }) - expect do - GitlabCiYamlProcessor.new(config) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "cache:key config should be a string or symbol") - end - - it "returns errors if job cache:key is not an a string" do - config = YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { key: 1 } } }) - expect do - GitlabCiYamlProcessor.new(config) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:cache:key config should be a string or symbol") - end - - it "returns errors if job cache:untracked is not an array of strings" do - config = YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { untracked: "string" } } }) - expect do - GitlabCiYamlProcessor.new(config) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:cache:untracked config should be a boolean value") - end - - it "returns errors if job cache:paths is not an array of strings" do - config = YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { paths: "string" } } }) - expect do - GitlabCiYamlProcessor.new(config) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:cache:paths config should be an array of strings") - end - - it "returns errors if job dependencies is not an array of strings" do - config = YAML.dump({ types: %w(build test), rspec: { script: "test", dependencies: "string" } }) - expect do - GitlabCiYamlProcessor.new(config) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec dependencies should be an array of strings") - end - end - - describe "Validate configuration templates" do - templates = Dir.glob("#{Rails.root.join('vendor/gitlab-ci-yml')}/**/*.gitlab-ci.yml") - - templates.each do |file| - it "does not return errors for #{file}" do - file = File.read(file) - - expect { GitlabCiYamlProcessor.new(file) }.not_to raise_error - end - end - end - - describe "#validation_message" do - context "when the YAML could not be parsed" do - it "returns an error about invalid configutaion" do - content = YAML.dump("invalid: yaml: test") - - expect(GitlabCiYamlProcessor.validation_message(content)) - .to eq "Invalid configuration format" - end - end - - context "when the tags parameter is invalid" do - it "returns an error about invalid tags" do - content = YAML.dump({ rspec: { script: "test", tags: "mysql" } }) - - expect(GitlabCiYamlProcessor.validation_message(content)) - .to eq "jobs:rspec tags should be an array of strings" - end - end - - context "when YAML content is empty" do - it "returns an error about missing content" do - expect(GitlabCiYamlProcessor.validation_message('')) - .to eq "Please provide content of .gitlab-ci.yml" - end - end - - context "when the YAML is valid" do - it "does not return any errors" do - content = File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) - - expect(GitlabCiYamlProcessor.validation_message(content)).to be_nil - end - end - end - end -end diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb index f685bb83d0d..4f4a27e4c41 100644 --- a/spec/lib/gitlab/auth_spec.rb +++ b/spec/lib/gitlab/auth_spec.rb @@ -17,11 +17,31 @@ describe Gitlab::Auth do end it 'OPTIONAL_SCOPES contains all non-default scopes' do + stub_container_registry_config(enabled: true) + expect(subject::OPTIONAL_SCOPES).to eq %i[read_user read_registry openid] end - it 'REGISTRY_SCOPES contains all registry related scopes' do - expect(subject::REGISTRY_SCOPES).to eq %i[read_registry] + context 'REGISTRY_SCOPES' do + context 'when registry is disabled' do + before do + stub_container_registry_config(enabled: false) + end + + it 'is empty' do + expect(subject::REGISTRY_SCOPES).to eq [] + end + end + + context 'when registry is enabled' do + before do + stub_container_registry_config(enabled: true) + end + + it 'contains all registry related scopes' do + expect(subject::REGISTRY_SCOPES).to eq %i[read_registry] + end + end end end @@ -147,11 +167,17 @@ describe Gitlab::Auth do expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(personal_access_token.user, nil, :personal_token, full_authentication_abilities)) end - it 'succeeds for personal access tokens with the `read_registry` scope' do - personal_access_token = create(:personal_access_token, scopes: ['read_registry']) + context 'when registry is enabled' do + before do + stub_container_registry_config(enabled: true) + end + + it 'succeeds for personal access tokens with the `read_registry` scope' do + personal_access_token = create(:personal_access_token, scopes: ['read_registry']) - expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: '') - expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(personal_access_token.user, nil, :personal_token, [:read_container_image])) + expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: '') + expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(personal_access_token.user, nil, :personal_token, [:read_container_image])) + end end it 'succeeds if it is an impersonation token' do diff --git a/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb b/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb index b155c20d8d3..cb52d971047 100644 --- a/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb +++ b/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb @@ -215,9 +215,17 @@ end # to a specific version of the database where said table is still present. # describe Gitlab::BackgroundMigration::MigrateEventsToPushEventPayloads, :migration, schema: 20170825154015 do + let(:user_class) do + Class.new(ActiveRecord::Base) do + self.table_name = 'users' + end + end + let(:migration) { described_class.new } - let(:project) { create(:project_empty_repo) } - let(:author) { create(:user) } + let(:user_class) { table(:users) } + let(:author) { build(:user).becomes(user_class).tap(&:save!).becomes(User) } + let(:namespace) { create(:namespace, owner: author) } + let(:project) { create(:project_empty_repo, namespace: namespace, creator: author) } # We can not rely on FactoryGirl as the state of Event may change in ways that # the background migration does not expect, hence we use the Event class of diff --git a/spec/lib/ci/ansi2html_spec.rb b/spec/lib/gitlab/ci/ansi2html_spec.rb index e49ecadde20..e6645985ba4 100644 --- a/spec/lib/ci/ansi2html_spec.rb +++ b/spec/lib/gitlab/ci/ansi2html_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Ci::Ansi2html do +describe Gitlab::Ci::Ansi2html do subject { described_class } it "prints non-ansi as-is" do diff --git a/spec/lib/ci/charts_spec.rb b/spec/lib/gitlab/ci/charts_spec.rb index f0769deef21..f8188675013 100644 --- a/spec/lib/ci/charts_spec.rb +++ b/spec/lib/gitlab/ci/charts_spec.rb @@ -1,9 +1,9 @@ require 'spec_helper' -describe Ci::Charts do +describe Gitlab::Ci::Charts do context "pipeline_times" do let(:project) { create(:project) } - let(:chart) { Ci::Charts::PipelineTime.new(project) } + let(:chart) { Gitlab::Ci::Charts::PipelineTime.new(project) } subject { chart.pipeline_times } diff --git a/spec/lib/ci/mask_secret_spec.rb b/spec/lib/gitlab/ci/mask_secret_spec.rb index f7b753b022b..3789a142248 100644 --- a/spec/lib/ci/mask_secret_spec.rb +++ b/spec/lib/gitlab/ci/mask_secret_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Ci::MaskSecret do +describe Gitlab::Ci::MaskSecret do subject { described_class } describe '#mask' do diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb new file mode 100644 index 00000000000..2278230f338 --- /dev/null +++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb @@ -0,0 +1,1699 @@ +require 'spec_helper' + +module Gitlab + module Ci + describe YamlProcessor, :lib do + subject { described_class.new(config, path) } + let(:path) { 'path' } + + describe 'our current .gitlab-ci.yml' do + let(:config) { File.read("#{Rails.root}/.gitlab-ci.yml") } + + it 'is valid' do + error_message = described_class.validation_message(config) + + expect(error_message).to be_nil + end + end + + describe '#build_attributes' do + subject { described_class.new(config, path).build_attributes(:rspec) } + + describe 'coverage entry' do + describe 'code coverage regexp' do + let(:config) do + YAML.dump(rspec: { script: 'rspec', + coverage: '/Code coverage: \d+\.\d+/' }) + end + + it 'includes coverage regexp in build attributes' do + expect(subject) + .to include(coverage_regex: 'Code coverage: \d+\.\d+') + end + end + end + + describe 'retry entry' do + context 'when retry count is specified' do + let(:config) do + YAML.dump(rspec: { script: 'rspec', retry: 1 }) + end + + it 'includes retry count in build options attribute' do + expect(subject[:options]).to include(retry: 1) + end + end + + context 'when retry count is not specified' do + let(:config) do + YAML.dump(rspec: { script: 'rspec' }) + end + + it 'does not persist retry count in the database' do + expect(subject[:options]).not_to have_key(:retry) + end + end + end + + describe 'allow failure entry' do + context 'when job is a manual action' do + context 'when allow_failure is defined' do + let(:config) do + YAML.dump(rspec: { script: 'rspec', + when: 'manual', + allow_failure: false }) + end + + it 'is not allowed to fail' do + expect(subject[:allow_failure]).to be false + end + end + + context 'when allow_failure is not defined' do + let(:config) do + YAML.dump(rspec: { script: 'rspec', + when: 'manual' }) + end + + it 'is allowed to fail' do + expect(subject[:allow_failure]).to be true + end + end + end + + context 'when job is not a manual action' do + context 'when allow_failure is defined' do + let(:config) do + YAML.dump(rspec: { script: 'rspec', + allow_failure: false }) + end + + it 'is not allowed to fail' do + expect(subject[:allow_failure]).to be false + end + end + + context 'when allow_failure is not defined' do + let(:config) do + YAML.dump(rspec: { script: 'rspec' }) + end + + it 'is not allowed to fail' do + expect(subject[:allow_failure]).to be false + end + end + end + end + end + + describe '#stage_seeds' do + context 'when no refs policy is specified' do + let(:config) do + YAML.dump(production: { stage: 'deploy', script: 'cap prod' }, + rspec: { stage: 'test', script: 'rspec' }, + spinach: { stage: 'test', script: 'spinach' }) + end + + let(:pipeline) { create(:ci_empty_pipeline) } + + it 'correctly fabricates a stage seeds object' do + seeds = subject.stage_seeds(pipeline) + + expect(seeds.size).to eq 2 + expect(seeds.first.stage[:name]).to eq 'test' + expect(seeds.second.stage[:name]).to eq 'deploy' + expect(seeds.first.builds.dig(0, :name)).to eq 'rspec' + expect(seeds.first.builds.dig(1, :name)).to eq 'spinach' + expect(seeds.second.builds.dig(0, :name)).to eq 'production' + end + end + + context 'when refs policy is specified' do + let(:config) do + YAML.dump(production: { stage: 'deploy', script: 'cap prod', only: ['master'] }, + spinach: { stage: 'test', script: 'spinach', only: ['tags'] }) + end + + let(:pipeline) do + create(:ci_empty_pipeline, ref: 'feature', tag: true) + end + + it 'returns stage seeds only assigned to master to master' do + seeds = subject.stage_seeds(pipeline) + + expect(seeds.size).to eq 1 + expect(seeds.first.stage[:name]).to eq 'test' + expect(seeds.first.builds.dig(0, :name)).to eq 'spinach' + end + end + + context 'when source policy is specified' do + let(:config) do + YAML.dump(production: { stage: 'deploy', script: 'cap prod', only: ['triggers'] }, + spinach: { stage: 'test', script: 'spinach', only: ['schedules'] }) + end + + let(:pipeline) do + create(:ci_empty_pipeline, source: :schedule) + end + + it 'returns stage seeds only assigned to schedules' do + seeds = subject.stage_seeds(pipeline) + + expect(seeds.size).to eq 1 + expect(seeds.first.stage[:name]).to eq 'test' + expect(seeds.first.builds.dig(0, :name)).to eq 'spinach' + end + end + + context 'when kubernetes policy is specified' do + let(:pipeline) { create(:ci_empty_pipeline) } + + let(:config) do + YAML.dump( + spinach: { stage: 'test', script: 'spinach' }, + production: { + stage: 'deploy', + script: 'cap', + only: { kubernetes: 'active' } + } + ) + end + + context 'when kubernetes is active' do + let(:project) { create(:kubernetes_project) } + let(:pipeline) { create(:ci_empty_pipeline, project: project) } + + it 'returns seeds for kubernetes dependent job' do + seeds = subject.stage_seeds(pipeline) + + expect(seeds.size).to eq 2 + expect(seeds.first.builds.dig(0, :name)).to eq 'spinach' + expect(seeds.second.builds.dig(0, :name)).to eq 'production' + end + end + + context 'when kubernetes is not active' do + it 'does not return seeds for kubernetes dependent job' do + seeds = subject.stage_seeds(pipeline) + + expect(seeds.size).to eq 1 + expect(seeds.first.builds.dig(0, :name)).to eq 'spinach' + end + end + end + end + + describe "#builds_for_stage_and_ref" do + let(:type) { 'test' } + + it "returns builds if no branch specified" do + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec" } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(1) + expect(config_processor.builds_for_stage_and_ref(type, "master").first).to eq({ + stage: "test", + stage_idx: 1, + name: "rspec", + commands: "pwd\nrspec", + coverage_regex: nil, + tag_list: [], + options: { + before_script: ["pwd"], + script: ["rspec"] + }, + allow_failure: false, + when: "on_success", + environment: nil, + yaml_variables: [] + }) + end + + describe 'only' do + it "does not return builds if only has another branch" do + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", only: ["deploy"] } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(0) + end + + it "does not return builds if only has regexp with another branch" do + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", only: ["/^deploy$/"] } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(0) + end + + it "returns builds if only has specified this branch" do + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", only: ["master"] } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(1) + end + + it "returns builds if only has a list of branches including specified" do + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", type: type, only: %w(master deploy) } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1) + end + + it "returns builds if only has a branches keyword specified" do + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", type: type, only: ["branches"] } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1) + end + + it "does not return builds if only has a tags keyword" do + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", type: type, only: ["tags"] } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0) + end + + it "returns builds if only has special keywords specified and source matches" do + possibilities = [{ keyword: 'pushes', source: 'push' }, + { keyword: 'web', source: 'web' }, + { keyword: 'triggers', source: 'trigger' }, + { keyword: 'schedules', source: 'schedule' }, + { keyword: 'api', source: 'api' }, + { keyword: 'external', source: 'external' }] + + possibilities.each do |possibility| + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", type: type, only: [possibility[:keyword]] } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref(type, "deploy", false, possibility[:source]).size).to eq(1) + end + end + + it "does not return builds if only has special keywords specified and source doesn't match" do + possibilities = [{ keyword: 'pushes', source: 'web' }, + { keyword: 'web', source: 'push' }, + { keyword: 'triggers', source: 'schedule' }, + { keyword: 'schedules', source: 'external' }, + { keyword: 'api', source: 'trigger' }, + { keyword: 'external', source: 'api' }] + + possibilities.each do |possibility| + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", type: type, only: [possibility[:keyword]] } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref(type, "deploy", false, possibility[:source]).size).to eq(0) + end + end + + it "returns builds if only has current repository path" do + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", type: type, only: ["branches@path"] } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1) + end + + it "does not return builds if only has different repository path" do + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", type: type, only: ["branches@fork"] } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0) + end + + it "returns build only for specified type" do + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", type: "test", only: %w(master deploy) }, + staging: { script: "deploy", type: "deploy", only: %w(master deploy) }, + production: { script: "deploy", type: "deploy", only: ["master@path", "deploy"] } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config, 'fork') + + expect(config_processor.builds_for_stage_and_ref("deploy", "deploy").size).to eq(2) + expect(config_processor.builds_for_stage_and_ref("test", "deploy").size).to eq(1) + expect(config_processor.builds_for_stage_and_ref("deploy", "master").size).to eq(1) + end + + context 'for invalid value' do + let(:config) { { rspec: { script: "rspec", type: "test", only: only } } } + let(:processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)) } + + context 'when it is integer' do + let(:only) { 1 } + + it do + expect { processor }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, + 'jobs:rspec:only has to be either an array of conditions or a hash') + end + end + + context 'when it is an array of integers' do + let(:only) { [1, 1] } + + it do + expect { processor }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, + 'jobs:rspec:only config should be an array of strings or regexps') + end + end + + context 'when it is invalid regex' do + let(:only) { ["/*invalid/"] } + + it do + expect { processor }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, + 'jobs:rspec:only config should be an array of strings or regexps') + end + end + end + end + + describe 'except' do + it "returns builds if except has another branch" do + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", except: ["deploy"] } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(1) + end + + it "returns builds if except has regexp with another branch" do + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", except: ["/^deploy$/"] } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(1) + end + + it "does not return builds if except has specified this branch" do + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", except: ["master"] } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(0) + end + + it "does not return builds if except has a list of branches including specified" do + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", type: type, except: %w(master deploy) } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0) + end + + it "does not return builds if except has a branches keyword specified" do + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", type: type, except: ["branches"] } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0) + end + + it "returns builds if except has a tags keyword" do + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", type: type, except: ["tags"] } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1) + end + + it "does not return builds if except has special keywords specified and source matches" do + possibilities = [{ keyword: 'pushes', source: 'push' }, + { keyword: 'web', source: 'web' }, + { keyword: 'triggers', source: 'trigger' }, + { keyword: 'schedules', source: 'schedule' }, + { keyword: 'api', source: 'api' }, + { keyword: 'external', source: 'external' }] + + possibilities.each do |possibility| + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", type: type, except: [possibility[:keyword]] } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref(type, "deploy", false, possibility[:source]).size).to eq(0) + end + end + + it "returns builds if except has special keywords specified and source doesn't match" do + possibilities = [{ keyword: 'pushes', source: 'web' }, + { keyword: 'web', source: 'push' }, + { keyword: 'triggers', source: 'schedule' }, + { keyword: 'schedules', source: 'external' }, + { keyword: 'api', source: 'trigger' }, + { keyword: 'external', source: 'api' }] + + possibilities.each do |possibility| + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", type: type, except: [possibility[:keyword]] } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref(type, "deploy", false, possibility[:source]).size).to eq(1) + end + end + + it "does not return builds if except has current repository path" do + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", type: type, except: ["branches@path"] } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0) + end + + it "returns builds if except has different repository path" do + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", type: type, except: ["branches@fork"] } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1) + end + + it "returns build except specified type" do + config = YAML.dump({ + before_script: ["pwd"], + rspec: { script: "rspec", type: "test", except: ["master", "deploy", "test@fork"] }, + staging: { script: "deploy", type: "deploy", except: ["master"] }, + production: { script: "deploy", type: "deploy", except: ["master@fork"] } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config, 'fork') + + expect(config_processor.builds_for_stage_and_ref("deploy", "deploy").size).to eq(2) + expect(config_processor.builds_for_stage_and_ref("test", "test").size).to eq(0) + expect(config_processor.builds_for_stage_and_ref("deploy", "master").size).to eq(0) + end + + context 'for invalid value' do + let(:config) { { rspec: { script: "rspec", except: except } } } + let(:processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)) } + + context 'when it is integer' do + let(:except) { 1 } + + it do + expect { processor }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, + 'jobs:rspec:except has to be either an array of conditions or a hash') + end + end + + context 'when it is an array of integers' do + let(:except) { [1, 1] } + + it do + expect { processor }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, + 'jobs:rspec:except config should be an array of strings or regexps') + end + end + + context 'when it is invalid regex' do + let(:except) { ["/*invalid/"] } + + it do + expect { processor }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, + 'jobs:rspec:except config should be an array of strings or regexps') + end + end + end + end + end + + describe "Scripts handling" do + let(:config_data) { YAML.dump(config) } + let(:config_processor) { Gitlab::Ci::YamlProcessor.new(config_data, path) } + + subject { config_processor.builds_for_stage_and_ref("test", "master").first } + + describe "before_script" do + context "in global context" do + let(:config) do + { + before_script: ["global script"], + test: { script: ["script"] } + } + end + + it "return commands with scripts concencaced" do + expect(subject[:commands]).to eq("global script\nscript") + end + end + + context "overwritten in local context" do + let(:config) do + { + before_script: ["global script"], + test: { before_script: ["local script"], script: ["script"] } + } + end + + it "return commands with scripts concencaced" do + expect(subject[:commands]).to eq("local script\nscript") + end + end + end + + describe "script" do + let(:config) do + { + test: { script: ["script"] } + } + end + + it "return commands with scripts concencaced" do + expect(subject[:commands]).to eq("script") + end + end + + describe "after_script" do + context "in global context" do + let(:config) do + { + after_script: ["after_script"], + test: { script: ["script"] } + } + end + + it "return after_script in options" do + expect(subject[:options][:after_script]).to eq(["after_script"]) + end + end + + context "overwritten in local context" do + let(:config) do + { + after_script: ["local after_script"], + test: { after_script: ["local after_script"], script: ["script"] } + } + end + + it "return after_script in options" do + expect(subject[:options][:after_script]).to eq(["local after_script"]) + end + end + end + end + + describe "Image and service handling" do + context "when extended docker configuration is used" do + it "returns image and service when defined" do + config = YAML.dump({ image: { name: "ruby:2.1", entrypoint: ["/usr/local/bin/init", "run"] }, + services: ["mysql", { name: "docker:dind", alias: "docker", + entrypoint: ["/usr/local/bin/init", "run"], + command: ["/usr/local/bin/init", "run"] }], + before_script: ["pwd"], + rspec: { script: "rspec" } }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) + expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({ + stage: "test", + stage_idx: 1, + name: "rspec", + commands: "pwd\nrspec", + coverage_regex: nil, + tag_list: [], + options: { + before_script: ["pwd"], + script: ["rspec"], + image: { name: "ruby:2.1", entrypoint: ["/usr/local/bin/init", "run"] }, + services: [{ name: "mysql" }, + { name: "docker:dind", alias: "docker", entrypoint: ["/usr/local/bin/init", "run"], + command: ["/usr/local/bin/init", "run"] }] + }, + allow_failure: false, + when: "on_success", + environment: nil, + yaml_variables: [] + }) + end + + it "returns image and service when overridden for job" do + config = YAML.dump({ image: "ruby:2.1", + services: ["mysql"], + before_script: ["pwd"], + rspec: { image: { name: "ruby:2.5", entrypoint: ["/usr/local/bin/init", "run"] }, + services: [{ name: "postgresql", alias: "db-pg", + entrypoint: ["/usr/local/bin/init", "run"], + command: ["/usr/local/bin/init", "run"] }, "docker:dind"], + script: "rspec" } }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) + expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({ + stage: "test", + stage_idx: 1, + name: "rspec", + commands: "pwd\nrspec", + coverage_regex: nil, + tag_list: [], + options: { + before_script: ["pwd"], + script: ["rspec"], + image: { name: "ruby:2.5", entrypoint: ["/usr/local/bin/init", "run"] }, + services: [{ name: "postgresql", alias: "db-pg", entrypoint: ["/usr/local/bin/init", "run"], + command: ["/usr/local/bin/init", "run"] }, + { name: "docker:dind" }] + }, + allow_failure: false, + when: "on_success", + environment: nil, + yaml_variables: [] + }) + end + end + + context "when etended docker configuration is not used" do + it "returns image and service when defined" do + config = YAML.dump({ image: "ruby:2.1", + services: ["mysql", "docker:dind"], + before_script: ["pwd"], + rspec: { script: "rspec" } }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) + expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({ + stage: "test", + stage_idx: 1, + name: "rspec", + commands: "pwd\nrspec", + coverage_regex: nil, + tag_list: [], + options: { + before_script: ["pwd"], + script: ["rspec"], + image: { name: "ruby:2.1" }, + services: [{ name: "mysql" }, { name: "docker:dind" }] + }, + allow_failure: false, + when: "on_success", + environment: nil, + yaml_variables: [] + }) + end + + it "returns image and service when overridden for job" do + config = YAML.dump({ image: "ruby:2.1", + services: ["mysql"], + before_script: ["pwd"], + rspec: { image: "ruby:2.5", services: ["postgresql", "docker:dind"], script: "rspec" } }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + + expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) + expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({ + stage: "test", + stage_idx: 1, + name: "rspec", + commands: "pwd\nrspec", + coverage_regex: nil, + tag_list: [], + options: { + before_script: ["pwd"], + script: ["rspec"], + image: { name: "ruby:2.5" }, + services: [{ name: "postgresql" }, { name: "docker:dind" }] + }, + allow_failure: false, + when: "on_success", + environment: nil, + yaml_variables: [] + }) + end + end + end + + describe 'Variables' do + let(:config_processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(config), path) } + + subject { config_processor.builds.first[:yaml_variables] } + + context 'when global variables are defined' do + let(:variables) do + { 'VAR1' => 'value1', 'VAR2' => 'value2' } + end + let(:config) do + { + variables: variables, + before_script: ['pwd'], + rspec: { script: 'rspec' } + } + end + + it 'returns global variables' do + expect(subject).to contain_exactly( + { key: 'VAR1', value: 'value1', public: true }, + { key: 'VAR2', value: 'value2', public: true } + ) + end + end + + context 'when job and global variables are defined' do + let(:global_variables) do + { 'VAR1' => 'global1', 'VAR3' => 'global3' } + end + let(:job_variables) do + { 'VAR1' => 'value1', 'VAR2' => 'value2' } + end + let(:config) do + { + before_script: ['pwd'], + variables: global_variables, + rspec: { script: 'rspec', variables: job_variables } + } + end + + it 'returns all unique variables' do + expect(subject).to contain_exactly( + { key: 'VAR3', value: 'global3', public: true }, + { key: 'VAR1', value: 'value1', public: true }, + { key: 'VAR2', value: 'value2', public: true } + ) + end + end + + context 'when job variables are defined' do + let(:config) do + { + before_script: ['pwd'], + rspec: { script: 'rspec', variables: variables } + } + end + + context 'when syntax is correct' do + let(:variables) do + { 'VAR1' => 'value1', 'VAR2' => 'value2' } + end + + it 'returns job variables' do + expect(subject).to contain_exactly( + { key: 'VAR1', value: 'value1', public: true }, + { key: 'VAR2', value: 'value2', public: true } + ) + end + end + + context 'when syntax is incorrect' do + context 'when variables defined but invalid' do + let(:variables) do + %w(VAR1 value1 VAR2 value2) + end + + it 'raises error' do + expect { subject } + .to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, + /jobs:rspec:variables config should be a hash of key value pairs/) + end + end + + context 'when variables key defined but value not specified' do + let(:variables) do + nil + end + + it 'returns empty array' do + ## + # When variables config is empty, we assume this is a valid + # configuration, see issue #18775 + # + expect(subject).to be_an_instance_of(Array) + expect(subject).to be_empty + end + end + end + end + + context 'when job variables are not defined' do + let(:config) do + { + before_script: ['pwd'], + rspec: { script: 'rspec' } + } + end + + it 'returns empty array' do + expect(subject).to be_an_instance_of(Array) + expect(subject).to be_empty + end + end + end + + describe "When" do + %w(on_success on_failure always).each do |when_state| + it "returns #{when_state} when defined" do + config = YAML.dump({ + rspec: { script: "rspec", when: when_state } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + + builds = config_processor.builds_for_stage_and_ref("test", "master") + expect(builds.size).to eq(1) + expect(builds.first[:when]).to eq(when_state) + end + end + end + + describe 'cache' do + context 'when cache definition has unknown keys' do + it 'raises relevant validation error' do + config = YAML.dump( + { cache: { untracked: true, invalid: 'key' }, + rspec: { script: 'rspec' } }) + + expect { Gitlab::Ci::YamlProcessor.new(config) }.to raise_error( + Gitlab::Ci::YamlProcessor::ValidationError, + 'cache config contains unknown keys: invalid' + ) + end + end + + it "returns cache when defined globally" do + config = YAML.dump({ + cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'key' }, + rspec: { + script: "rspec" + } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config) + + expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) + expect(config_processor.builds_for_stage_and_ref("test", "master").first[:options][:cache]).to eq( + paths: ["logs/", "binaries/"], + untracked: true, + key: 'key', + policy: 'pull-push' + ) + end + + it "returns cache when defined in a job" do + config = YAML.dump({ + rspec: { + cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'key' }, + script: "rspec" + } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config) + + expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) + expect(config_processor.builds_for_stage_and_ref("test", "master").first[:options][:cache]).to eq( + paths: ["logs/", "binaries/"], + untracked: true, + key: 'key', + policy: 'pull-push' + ) + end + + it "overwrite cache when defined for a job and globally" do + config = YAML.dump({ + cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'global' }, + rspec: { + script: "rspec", + cache: { paths: ["test/"], untracked: false, key: 'local' } + } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config) + + expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) + expect(config_processor.builds_for_stage_and_ref("test", "master").first[:options][:cache]).to eq( + paths: ["test/"], + untracked: false, + key: 'local', + policy: 'pull-push' + ) + end + end + + describe "Artifacts" do + it "returns artifacts when defined" do + config = YAML.dump({ + image: "ruby:2.1", + services: ["mysql"], + before_script: ["pwd"], + rspec: { + artifacts: { + paths: ["logs/", "binaries/"], + untracked: true, + name: "custom_name", + expire_in: "7d" + }, + script: "rspec" + } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config) + + expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1) + expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({ + stage: "test", + stage_idx: 1, + name: "rspec", + commands: "pwd\nrspec", + coverage_regex: nil, + tag_list: [], + options: { + before_script: ["pwd"], + script: ["rspec"], + image: { name: "ruby:2.1" }, + services: [{ name: "mysql" }], + artifacts: { + name: "custom_name", + paths: ["logs/", "binaries/"], + untracked: true, + expire_in: "7d" + } + }, + when: "on_success", + allow_failure: false, + environment: nil, + yaml_variables: [] + }) + end + + %w[on_success on_failure always].each do |when_state| + it "returns artifacts for when #{when_state} defined" do + config = YAML.dump({ + rspec: { + script: "rspec", + artifacts: { paths: ["logs/", "binaries/"], when: when_state } + } + }) + + config_processor = Gitlab::Ci::YamlProcessor.new(config, path) + + builds = config_processor.builds_for_stage_and_ref("test", "master") + expect(builds.size).to eq(1) + expect(builds.first[:options][:artifacts][:when]).to eq(when_state) + end + end + end + + describe '#environment' do + let(:config) do + { + deploy_to_production: { stage: 'deploy', script: 'test', environment: environment } + } + end + + let(:processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)) } + let(:builds) { processor.builds_for_stage_and_ref('deploy', 'master') } + + context 'when a production environment is specified' do + let(:environment) { 'production' } + + it 'does return production' do + expect(builds.size).to eq(1) + expect(builds.first[:environment]).to eq(environment) + expect(builds.first[:options]).to include(environment: { name: environment, action: "start" }) + end + end + + context 'when hash is specified' do + let(:environment) do + { name: 'production', + url: 'http://production.gitlab.com' } + end + + it 'does return production and URL' do + expect(builds.size).to eq(1) + expect(builds.first[:environment]).to eq(environment[:name]) + expect(builds.first[:options]).to include(environment: environment) + end + + context 'the url has a port as variable' do + let(:environment) do + { name: 'production', + url: 'http://production.gitlab.com:$PORT' } + end + + it 'allows a variable for the port' do + expect(builds.size).to eq(1) + expect(builds.first[:environment]).to eq(environment[:name]) + expect(builds.first[:options]).to include(environment: environment) + end + end + end + + context 'when no environment is specified' do + let(:environment) { nil } + + it 'does return nil environment' do + expect(builds.size).to eq(1) + expect(builds.first[:environment]).to be_nil + end + end + + context 'is not a string' do + let(:environment) { 1 } + + it 'raises error' do + expect { builds }.to raise_error( + 'jobs:deploy_to_production:environment config should be a hash or a string') + end + end + + context 'is not a valid string' do + let(:environment) { 'production:staging' } + + it 'raises error' do + expect { builds }.to raise_error("jobs:deploy_to_production:environment name #{Gitlab::Regex.environment_name_regex_message}") + end + end + + context 'when on_stop is specified' do + let(:review) { { stage: 'deploy', script: 'test', environment: { name: 'review', on_stop: 'close_review' } } } + let(:config) { { review: review, close_review: close_review }.compact } + + context 'with matching job' do + let(:close_review) { { stage: 'deploy', script: 'test', environment: { name: 'review', action: 'stop' } } } + + it 'does return a list of builds' do + expect(builds.size).to eq(2) + expect(builds.first[:environment]).to eq('review') + end + end + + context 'without matching job' do + let(:close_review) { nil } + + it 'raises error' do + expect { builds }.to raise_error('review job: on_stop job close_review is not defined') + end + end + + context 'with close job without environment' do + let(:close_review) { { stage: 'deploy', script: 'test' } } + + it 'raises error' do + expect { builds }.to raise_error('review job: on_stop job close_review does not have environment defined') + end + end + + context 'with close job for different environment' do + let(:close_review) { { stage: 'deploy', script: 'test', environment: 'production' } } + + it 'raises error' do + expect { builds }.to raise_error('review job: on_stop job close_review have different environment name') + end + end + + context 'with close job without stop action' do + let(:close_review) { { stage: 'deploy', script: 'test', environment: { name: 'review' } } } + + it 'raises error' do + expect { builds }.to raise_error('review job: on_stop job close_review needs to have action stop defined') + end + end + end + end + + describe "Dependencies" do + let(:config) do + { + build1: { stage: 'build', script: 'test' }, + build2: { stage: 'build', script: 'test' }, + test1: { stage: 'test', script: 'test', dependencies: dependencies }, + test2: { stage: 'test', script: 'test' }, + deploy: { stage: 'test', script: 'test' } + } + end + + subject { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)) } + + context 'no dependencies' do + let(:dependencies) { } + + it { expect { subject }.not_to raise_error } + end + + context 'dependencies to builds' do + let(:dependencies) { %w(build1 build2) } + + it { expect { subject }.not_to raise_error } + end + + context 'dependencies to builds defined as symbols' do + let(:dependencies) { [:build1, :build2] } + + it { expect { subject }.not_to raise_error } + end + + context 'undefined dependency' do + let(:dependencies) { ['undefined'] } + + it { expect { subject }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, 'test1 job: undefined dependency: undefined') } + end + + context 'dependencies to deploy' do + let(:dependencies) { ['deploy'] } + + it { expect { subject }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, 'test1 job: dependency deploy is not defined in prior stages') } + end + end + + describe "Hidden jobs" do + let(:config_processor) { Gitlab::Ci::YamlProcessor.new(config) } + subject { config_processor.builds_for_stage_and_ref("test", "master") } + + shared_examples 'hidden_job_handling' do + it "doesn't create jobs that start with dot" do + expect(subject.size).to eq(1) + expect(subject.first).to eq({ + stage: "test", + stage_idx: 1, + name: "normal_job", + commands: "test", + coverage_regex: nil, + tag_list: [], + options: { + script: ["test"] + }, + when: "on_success", + allow_failure: false, + environment: nil, + yaml_variables: [] + }) + end + end + + context 'when hidden job have a script definition' do + let(:config) do + YAML.dump({ + '.hidden_job' => { image: 'ruby:2.1', script: 'test' }, + 'normal_job' => { script: 'test' } + }) + end + + it_behaves_like 'hidden_job_handling' + end + + context "when hidden job doesn't have a script definition" do + let(:config) do + YAML.dump({ + '.hidden_job' => { image: 'ruby:2.1' }, + 'normal_job' => { script: 'test' } + }) + end + + it_behaves_like 'hidden_job_handling' + end + end + + describe "YAML Alias/Anchor" do + let(:config_processor) { Gitlab::Ci::YamlProcessor.new(config) } + subject { config_processor.builds_for_stage_and_ref("build", "master") } + + shared_examples 'job_templates_handling' do + it "is correctly supported for jobs" do + expect(subject.size).to eq(2) + expect(subject.first).to eq({ + stage: "build", + stage_idx: 0, + name: "job1", + commands: "execute-script-for-job", + coverage_regex: nil, + tag_list: [], + options: { + script: ["execute-script-for-job"] + }, + when: "on_success", + allow_failure: false, + environment: nil, + yaml_variables: [] + }) + expect(subject.second).to eq({ + stage: "build", + stage_idx: 0, + name: "job2", + commands: "execute-script-for-job", + coverage_regex: nil, + tag_list: [], + options: { + script: ["execute-script-for-job"] + }, + when: "on_success", + allow_failure: false, + environment: nil, + yaml_variables: [] + }) + end + end + + context 'when template is a job' do + let(:config) do + <<EOT +job1: &JOBTMPL + stage: build + script: execute-script-for-job + +job2: *JOBTMPL +EOT + end + + it_behaves_like 'job_templates_handling' + end + + context 'when template is a hidden job' do + let(:config) do + <<EOT +.template: &JOBTMPL + stage: build + script: execute-script-for-job + +job1: *JOBTMPL + +job2: *JOBTMPL +EOT + end + + it_behaves_like 'job_templates_handling' + end + + context 'when job adds its own keys to a template definition' do + let(:config) do + <<EOT +.template: &JOBTMPL + stage: build + +job1: + <<: *JOBTMPL + script: execute-script-for-job + +job2: + <<: *JOBTMPL + script: execute-script-for-job +EOT + end + + it_behaves_like 'job_templates_handling' + end + end + + describe "Error handling" do + it "fails to parse YAML" do + expect {Gitlab::Ci::YamlProcessor.new("invalid: yaml: test")}.to raise_error(Psych::SyntaxError) + end + + it "indicates that object is invalid" do + expect {Gitlab::Ci::YamlProcessor.new("invalid_yaml")}.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError) + end + + it "returns errors if tags parameter is invalid" do + config = YAML.dump({ rspec: { script: "test", tags: "mysql" } }) + expect do + Gitlab::Ci::YamlProcessor.new(config, path) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec tags should be an array of strings") + end + + it "returns errors if before_script parameter is invalid" do + config = YAML.dump({ before_script: "bundle update", rspec: { script: "test" } }) + expect do + Gitlab::Ci::YamlProcessor.new(config, path) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "before_script config should be an array of strings") + end + + it "returns errors if job before_script parameter is not an array of strings" do + config = YAML.dump({ rspec: { script: "test", before_script: [10, "test"] } }) + expect do + Gitlab::Ci::YamlProcessor.new(config, path) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:before_script config should be an array of strings") + end + + it "returns errors if after_script parameter is invalid" do + config = YAML.dump({ after_script: "bundle update", rspec: { script: "test" } }) + expect do + Gitlab::Ci::YamlProcessor.new(config, path) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "after_script config should be an array of strings") + end + + it "returns errors if job after_script parameter is not an array of strings" do + config = YAML.dump({ rspec: { script: "test", after_script: [10, "test"] } }) + expect do + Gitlab::Ci::YamlProcessor.new(config, path) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:after_script config should be an array of strings") + end + + it "returns errors if image parameter is invalid" do + config = YAML.dump({ image: ["test"], rspec: { script: "test" } }) + expect do + Gitlab::Ci::YamlProcessor.new(config, path) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "image config should be a hash or a string") + end + + it "returns errors if job name is blank" do + config = YAML.dump({ '' => { script: "test" } }) + expect do + Gitlab::Ci::YamlProcessor.new(config, path) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:job name can't be blank") + end + + it "returns errors if job name is non-string" do + config = YAML.dump({ 10 => { script: "test" } }) + expect do + Gitlab::Ci::YamlProcessor.new(config, path) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:10 name should be a symbol") + end + + it "returns errors if job image parameter is invalid" do + config = YAML.dump({ rspec: { script: "test", image: ["test"] } }) + expect do + Gitlab::Ci::YamlProcessor.new(config, path) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:image config should be a hash or a string") + end + + it "returns errors if services parameter is not an array" do + config = YAML.dump({ services: "test", rspec: { script: "test" } }) + expect do + Gitlab::Ci::YamlProcessor.new(config, path) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "services config should be a array") + end + + it "returns errors if services parameter is not an array of strings" do + config = YAML.dump({ services: [10, "test"], rspec: { script: "test" } }) + expect do + Gitlab::Ci::YamlProcessor.new(config, path) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "service config should be a hash or a string") + end + + it "returns errors if job services parameter is not an array" do + config = YAML.dump({ rspec: { script: "test", services: "test" } }) + expect do + Gitlab::Ci::YamlProcessor.new(config, path) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:services config should be a array") + end + + it "returns errors if job services parameter is not an array of strings" do + config = YAML.dump({ rspec: { script: "test", services: [10, "test"] } }) + expect do + Gitlab::Ci::YamlProcessor.new(config, path) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "service config should be a hash or a string") + end + + it "returns error if job configuration is invalid" do + config = YAML.dump({ extra: "bundle update" }) + expect do + Gitlab::Ci::YamlProcessor.new(config, path) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:extra config should be a hash") + end + + it "returns errors if services configuration is not correct" do + config = YAML.dump({ extra: { script: 'rspec', services: "test" } }) + expect do + Gitlab::Ci::YamlProcessor.new(config, path) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:extra:services config should be a array") + end + + it "returns errors if there are no jobs defined" do + config = YAML.dump({ before_script: ["bundle update"] }) + expect do + Gitlab::Ci::YamlProcessor.new(config, path) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs config should contain at least one visible job") + end + + it "returns errors if there are no visible jobs defined" do + config = YAML.dump({ before_script: ["bundle update"], '.hidden'.to_sym => { script: 'ls' } }) + expect do + Gitlab::Ci::YamlProcessor.new(config, path) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs config should contain at least one visible job") + end + + it "returns errors if job allow_failure parameter is not an boolean" do + config = YAML.dump({ rspec: { script: "test", allow_failure: "string" } }) + expect do + Gitlab::Ci::YamlProcessor.new(config, path) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec allow failure should be a boolean value") + end + + it "returns errors if job stage is not a string" do + config = YAML.dump({ rspec: { script: "test", type: 1 } }) + expect do + Gitlab::Ci::YamlProcessor.new(config, path) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:type config should be a string") + end + + it "returns errors if job stage is not a pre-defined stage" do + config = YAML.dump({ rspec: { script: "test", type: "acceptance" } }) + expect do + Gitlab::Ci::YamlProcessor.new(config, path) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "rspec job: stage parameter should be build, test, deploy") + end + + it "returns errors if job stage is not a defined stage" do + config = YAML.dump({ types: %w(build test), rspec: { script: "test", type: "acceptance" } }) + expect do + Gitlab::Ci::YamlProcessor.new(config, path) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "rspec job: stage parameter should be build, test") + end + + it "returns errors if stages is not an array" do + config = YAML.dump({ stages: "test", rspec: { script: "test" } }) + expect do + Gitlab::Ci::YamlProcessor.new(config, path) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "stages config should be an array of strings") + end + + it "returns errors if stages is not an array of strings" do + config = YAML.dump({ stages: [true, "test"], rspec: { script: "test" } }) + expect do + Gitlab::Ci::YamlProcessor.new(config, path) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "stages config should be an array of strings") + end + + it "returns errors if variables is not a map" do + config = YAML.dump({ variables: "test", rspec: { script: "test" } }) + expect do + Gitlab::Ci::YamlProcessor.new(config, path) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "variables config should be a hash of key value pairs") + end + + it "returns errors if variables is not a map of key-value strings" do + config = YAML.dump({ variables: { test: false }, rspec: { script: "test" } }) + expect do + Gitlab::Ci::YamlProcessor.new(config, path) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "variables config should be a hash of key value pairs") + end + + it "returns errors if job when is not on_success, on_failure or always" do + config = YAML.dump({ rspec: { script: "test", when: 1 } }) + expect do + Gitlab::Ci::YamlProcessor.new(config, path) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec when should be on_success, on_failure, always or manual") + end + + it "returns errors if job artifacts:name is not an a string" do + config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { name: 1 } } }) + expect do + Gitlab::Ci::YamlProcessor.new(config) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:artifacts name should be a string") + end + + it "returns errors if job artifacts:when is not an a predefined value" do + config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { when: 1 } } }) + expect do + Gitlab::Ci::YamlProcessor.new(config) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:artifacts when should be on_success, on_failure or always") + end + + it "returns errors if job artifacts:expire_in is not an a string" do + config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { expire_in: 1 } } }) + expect do + Gitlab::Ci::YamlProcessor.new(config) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:artifacts expire in should be a duration") + end + + it "returns errors if job artifacts:expire_in is not an a valid duration" do + config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { expire_in: "7 elephants" } } }) + expect do + Gitlab::Ci::YamlProcessor.new(config) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:artifacts expire in should be a duration") + end + + it "returns errors if job artifacts:untracked is not an array of strings" do + config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { untracked: "string" } } }) + expect do + Gitlab::Ci::YamlProcessor.new(config) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:artifacts untracked should be a boolean value") + end + + it "returns errors if job artifacts:paths is not an array of strings" do + config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { paths: "string" } } }) + expect do + Gitlab::Ci::YamlProcessor.new(config) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:artifacts paths should be an array of strings") + end + + it "returns errors if cache:untracked is not an array of strings" do + config = YAML.dump({ cache: { untracked: "string" }, rspec: { script: "test" } }) + expect do + Gitlab::Ci::YamlProcessor.new(config) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "cache:untracked config should be a boolean value") + end + + it "returns errors if cache:paths is not an array of strings" do + config = YAML.dump({ cache: { paths: "string" }, rspec: { script: "test" } }) + expect do + Gitlab::Ci::YamlProcessor.new(config) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "cache:paths config should be an array of strings") + end + + it "returns errors if cache:key is not a string" do + config = YAML.dump({ cache: { key: 1 }, rspec: { script: "test" } }) + expect do + Gitlab::Ci::YamlProcessor.new(config) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "cache:key config should be a string or symbol") + end + + it "returns errors if job cache:key is not an a string" do + config = YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { key: 1 } } }) + expect do + Gitlab::Ci::YamlProcessor.new(config) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:cache:key config should be a string or symbol") + end + + it "returns errors if job cache:untracked is not an array of strings" do + config = YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { untracked: "string" } } }) + expect do + Gitlab::Ci::YamlProcessor.new(config) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:cache:untracked config should be a boolean value") + end + + it "returns errors if job cache:paths is not an array of strings" do + config = YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { paths: "string" } } }) + expect do + Gitlab::Ci::YamlProcessor.new(config) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:cache:paths config should be an array of strings") + end + + it "returns errors if job dependencies is not an array of strings" do + config = YAML.dump({ types: %w(build test), rspec: { script: "test", dependencies: "string" } }) + expect do + Gitlab::Ci::YamlProcessor.new(config) + end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec dependencies should be an array of strings") + end + end + + describe "Validate configuration templates" do + templates = Dir.glob("#{Rails.root.join('vendor/gitlab-ci-yml')}/**/*.gitlab-ci.yml") + + templates.each do |file| + it "does not return errors for #{file}" do + file = File.read(file) + + expect { Gitlab::Ci::YamlProcessor.new(file) }.not_to raise_error + end + end + end + + describe "#validation_message" do + context "when the YAML could not be parsed" do + it "returns an error about invalid configutaion" do + content = YAML.dump("invalid: yaml: test") + + expect(Gitlab::Ci::YamlProcessor.validation_message(content)) + .to eq "Invalid configuration format" + end + end + + context "when the tags parameter is invalid" do + it "returns an error about invalid tags" do + content = YAML.dump({ rspec: { script: "test", tags: "mysql" } }) + + expect(Gitlab::Ci::YamlProcessor.validation_message(content)) + .to eq "jobs:rspec tags should be an array of strings" + end + end + + context "when YAML content is empty" do + it "returns an error about missing content" do + expect(Gitlab::Ci::YamlProcessor.validation_message('')) + .to eq "Please provide content of .gitlab-ci.yml" + end + end + + context "when the YAML is valid" do + it "does not return any errors" do + content = File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) + + expect(Gitlab::Ci::YamlProcessor.validation_message(content)).to be_nil + end + end + end + end + end +end diff --git a/spec/lib/gitlab/data_builder/push_spec.rb b/spec/lib/gitlab/data_builder/push_spec.rb index cb430b47463..befdc18d1aa 100644 --- a/spec/lib/gitlab/data_builder/push_spec.rb +++ b/spec/lib/gitlab/data_builder/push_spec.rb @@ -47,7 +47,7 @@ describe Gitlab::DataBuilder::Push do include_examples 'deprecated repository hook data' it 'does not raise an error when given nil commits' do - expect { described_class.build(spy, spy, spy, spy, spy, nil) } + expect { described_class.build(spy, spy, spy, spy, 'refs/tags/v1.1.0', nil) } .not_to raise_error end end diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb index 14d64d8c4da..46e968cc398 100644 --- a/spec/lib/gitlab/git/commit_spec.rb +++ b/spec/lib/gitlab/git/commit_spec.rb @@ -401,7 +401,7 @@ describe Gitlab::Git::Commit, seed_helper: true do end end - describe '#stats' do + shared_examples '#stats' do subject { commit.stats } describe '#additions' do @@ -415,6 +415,14 @@ describe Gitlab::Git::Commit, seed_helper: true do end end + describe '#stats with gitaly on' do + it_should_behave_like '#stats' + end + + describe '#stats with gitaly disabled', skip_gitaly_mock: true do + it_should_behave_like '#stats' + end + describe '#to_diff' do subject { commit.to_diff } diff --git a/spec/lib/gitlab/git/hooks_service_spec.rb b/spec/lib/gitlab/git/hooks_service_spec.rb index e9c0209fe3b..d4d75b66659 100644 --- a/spec/lib/gitlab/git/hooks_service_spec.rb +++ b/spec/lib/gitlab/git/hooks_service_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Gitlab::Git::HooksService, seed_helper: true do - let(:committer) { Gitlab::Git::Committer.new('Jane Doe', 'janedoe@example.com', 'user-456') } + let(:user) { Gitlab::Git::User.new('Jane Doe', 'janedoe@example.com', 'user-456') } let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, 'project-123') } let(:service) { described_class.new } @@ -18,7 +18,7 @@ describe Gitlab::Git::HooksService, seed_helper: true do hook = double(trigger: [true, nil]) expect(Gitlab::Git::Hook).to receive(:new).exactly(3).times.and_return(hook) - service.execute(committer, repository, @blankrev, @newrev, @ref) { } + service.execute(user, repository, @blankrev, @newrev, @ref) { } end end @@ -28,7 +28,7 @@ describe Gitlab::Git::HooksService, seed_helper: true do expect(service).not_to receive(:run_hook).with('post-receive') expect do - service.execute(committer, repository, @blankrev, @newrev, @ref) + service.execute(user, repository, @blankrev, @newrev, @ref) end.to raise_error(Gitlab::Git::HooksService::PreReceiveError) end end @@ -40,7 +40,7 @@ describe Gitlab::Git::HooksService, seed_helper: true do expect(service).not_to receive(:run_hook).with('post-receive') expect do - service.execute(committer, repository, @blankrev, @newrev, @ref) + service.execute(user, repository, @blankrev, @newrev, @ref) end.to raise_error(Gitlab::Git::HooksService::PreReceiveError) end end diff --git a/spec/lib/gitlab/git/committer_spec.rb b/spec/lib/gitlab/git/user_spec.rb index b0ddbb51449..0ebcecb26c0 100644 --- a/spec/lib/gitlab/git/committer_spec.rb +++ b/spec/lib/gitlab/git/user_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::Git::Committer do +describe Gitlab::Git::User do let(:name) { 'Jane Doe' } let(:email) { 'janedoe@example.com' } let(:gl_id) { 'user-123' } diff --git a/spec/lib/gitlab/git_spec.rb b/spec/lib/gitlab/git_spec.rb index 4702a978f19..494dfe0e595 100644 --- a/spec/lib/gitlab/git_spec.rb +++ b/spec/lib/gitlab/git_spec.rb @@ -1,3 +1,4 @@ +# coding: utf-8 require 'spec_helper' describe Gitlab::Git do @@ -29,4 +30,12 @@ describe Gitlab::Git do end end end + + describe '.ref_name' do + it 'ensure ref is a valid UTF-8 string' do + utf8_invalid_ref = Gitlab::Git::BRANCH_REF_PREFIX + "an_invalid_ref_\xE5" + + expect(described_class.ref_name(utf8_invalid_ref)).to eq("an_invalid_ref_å") + end + end end diff --git a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb index f32fe5d8150..ec3abcb0953 100644 --- a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb @@ -165,4 +165,29 @@ describe Gitlab::GitalyClient::CommitService do expect(subject).to eq("my diff") end end + + describe '#commit_stats' do + let(:request) do + Gitaly::CommitStatsRequest.new( + repository: repository_message, revision: revision + ) + end + let(:response) do + Gitaly::CommitStatsResponse.new( + oid: revision, + additions: 11, + deletions: 15 + ) + end + + subject { described_class.new(repository).commit_stats(revision) } + + it 'sends an RPC request' do + expect_any_instance_of(Gitaly::CommitService::Stub).to receive(:commit_stats) + .with(request, kind_of(Hash)).and_return(response) + + expect(subject.additions).to eq(11) + expect(subject.deletions).to eq(15) + end + end end diff --git a/spec/lib/gitlab/group_hierarchy_spec.rb b/spec/lib/gitlab/group_hierarchy_spec.rb index 08010c2d0e2..8dc83a6db7f 100644 --- a/spec/lib/gitlab/group_hierarchy_spec.rb +++ b/spec/lib/gitlab/group_hierarchy_spec.rb @@ -23,6 +23,11 @@ describe Gitlab::GroupHierarchy, :postgresql do expect(relation).to include(parent, child1, child2) end + + it 'does not allow the use of #update_all' do + expect { relation.update_all(share_with_group_lock: false) } + .to raise_error(ActiveRecord::ReadOnlyRecord) + end end describe '#base_and_descendants' do @@ -43,6 +48,11 @@ describe Gitlab::GroupHierarchy, :postgresql do expect(relation).to include(parent, child1, child2) end + + it 'does not allow the use of #update_all' do + expect { relation.update_all(share_with_group_lock: false) } + .to raise_error(ActiveRecord::ReadOnlyRecord) + end end describe '#all_groups' do @@ -73,5 +83,10 @@ describe Gitlab::GroupHierarchy, :postgresql do expect(relation).to include(child2) end + + it 'does not allow the use of #update_all' do + expect { relation.update_all(share_with_group_lock: false) } + .to raise_error(ActiveRecord::ReadOnlyRecord) + end end end diff --git a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb index 8e3554375e8..d9b86e1bf34 100644 --- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb @@ -119,7 +119,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver do it 'has no when YML attributes but only the DB column' do allow_any_instance_of(Ci::Pipeline).to receive(:ci_yaml_file).and_return(File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml'))) - expect_any_instance_of(Ci::GitlabCiYamlProcessor).not_to receive(:build_attributes) + expect_any_instance_of(Gitlab::Ci::YamlProcessor).not_to receive(:build_attributes) saved_project_json end diff --git a/spec/lib/gitlab/o_auth/auth_hash_spec.rb b/spec/lib/gitlab/o_auth/auth_hash_spec.rb index d5f4da3ce36..dbcc200b90b 100644 --- a/spec/lib/gitlab/o_auth/auth_hash_spec.rb +++ b/spec/lib/gitlab/o_auth/auth_hash_spec.rb @@ -1,10 +1,11 @@ require 'spec_helper' describe Gitlab::OAuth::AuthHash do + let(:provider) { 'ldap'.freeze } let(:auth_hash) do described_class.new( OmniAuth::AuthHash.new( - provider: provider_ascii, + provider: provider, uid: uid_ascii, info: info_hash ) @@ -20,7 +21,6 @@ describe Gitlab::OAuth::AuthHash do let(:last_name_raw) { "K\xC3\xBC\xC3\xA7\xC3\xBCk" } let(:name_raw) { "Onur K\xC3\xBC\xC3\xA7\xC3\xBCk" } - let(:provider_ascii) { 'ldap'.force_encoding(Encoding::ASCII_8BIT) } let(:uid_ascii) { uid_raw.force_encoding(Encoding::ASCII_8BIT) } let(:email_ascii) { email_raw.force_encoding(Encoding::ASCII_8BIT) } let(:nickname_ascii) { nickname_raw.force_encoding(Encoding::ASCII_8BIT) } @@ -28,7 +28,6 @@ describe Gitlab::OAuth::AuthHash do let(:last_name_ascii) { last_name_raw.force_encoding(Encoding::ASCII_8BIT) } let(:name_ascii) { name_raw.force_encoding(Encoding::ASCII_8BIT) } - let(:provider_utf8) { provider_ascii.force_encoding(Encoding::UTF_8) } let(:uid_utf8) { uid_ascii.force_encoding(Encoding::UTF_8) } let(:email_utf8) { email_ascii.force_encoding(Encoding::UTF_8) } let(:nickname_utf8) { nickname_ascii.force_encoding(Encoding::UTF_8) } @@ -46,7 +45,7 @@ describe Gitlab::OAuth::AuthHash do end context 'defaults' do - it { expect(auth_hash.provider).to eql provider_utf8 } + it { expect(auth_hash.provider).to eq provider } it { expect(auth_hash.uid).to eql uid_utf8 } it { expect(auth_hash.email).to eql email_utf8 } it { expect(auth_hash.username).to eql nickname_utf8 } diff --git a/spec/lib/gitlab/themes_spec.rb b/spec/lib/gitlab/themes_spec.rb new file mode 100644 index 00000000000..ecacea6bb35 --- /dev/null +++ b/spec/lib/gitlab/themes_spec.rb @@ -0,0 +1,48 @@ +require 'spec_helper' + +describe Gitlab::Themes, lib: true do + describe '.body_classes' do + it 'returns a space-separated list of class names' do + css = described_class.body_classes + + expect(css).to include('ui_indigo') + expect(css).to include(' ui_dark ') + expect(css).to include(' ui_blue') + end + end + + describe '.by_id' do + it 'returns a Theme by its ID' do + expect(described_class.by_id(1).name).to eq 'Indigo' + expect(described_class.by_id(3).name).to eq 'Light' + end + end + + describe '.default' do + it 'returns the default application theme' do + allow(described_class).to receive(:default_id).and_return(2) + expect(described_class.default.id).to eq 2 + end + + it 'prevents an infinite loop when configuration default is invalid' do + default = described_class::APPLICATION_DEFAULT + themes = described_class::THEMES + + config = double(default_theme: 0).as_null_object + allow(Gitlab).to receive(:config).and_return(config) + expect(described_class.default.id).to eq default + + config = double(default_theme: themes.size + 5).as_null_object + allow(Gitlab).to receive(:config).and_return(config) + expect(described_class.default.id).to eq default + end + end + + describe '.each' do + it 'passes the block to the THEMES Array' do + ids = [] + described_class.each { |theme| ids << theme.id } + expect(ids).not_to be_empty + end + end +end diff --git a/spec/lib/gitlab/url_sanitizer_spec.rb b/spec/lib/gitlab/url_sanitizer_spec.rb index fdc3990132a..59c28431e1e 100644 --- a/spec/lib/gitlab/url_sanitizer_spec.rb +++ b/spec/lib/gitlab/url_sanitizer_spec.rb @@ -174,4 +174,13 @@ describe Gitlab::UrlSanitizer do end end end + + context 'when credentials contains special chars' do + it 'should parse the URL without errors' do + url_sanitizer = described_class.new("https://foo:b?r@github.com/me/project.git") + + expect(url_sanitizer.sanitized_url).to eq("https://github.com/me/project.git") + expect(url_sanitizer.full_url).to eq("https://foo:b?r@github.com/me/project.git") + end + end end diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index 68429d792f2..c7d9f105f04 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -40,9 +40,13 @@ describe Gitlab::UsageData do ci_builds ci_internal_pipelines ci_external_pipelines + ci_pipeline_config_auto_devops + ci_pipeline_config_repository ci_runners ci_triggers ci_pipeline_schedules + auto_devops_enabled + auto_devops_disabled deploy_keys deployments environments diff --git a/spec/lib/system_check/orphans/namespace_check_spec.rb b/spec/lib/system_check/orphans/namespace_check_spec.rb new file mode 100644 index 00000000000..2a61ff3ad65 --- /dev/null +++ b/spec/lib/system_check/orphans/namespace_check_spec.rb @@ -0,0 +1,61 @@ +require 'spec_helper' +require 'rake_helper' + +describe SystemCheck::Orphans::NamespaceCheck do + let(:storages) { Gitlab.config.repositories.storages.reject { |key, _| key.eql? 'broken' } } + + before do + allow(Gitlab.config.repositories).to receive(:storages).and_return(storages) + allow(subject).to receive(:fetch_disk_namespaces).and_return(disk_namespaces) + silence_output + end + + describe '#multi_check' do + context 'all orphans' do + let(:disk_namespaces) { %w(/repos/orphan1 /repos/orphan2 repos/@hashed) } + + it 'prints list of all orphaned namespaces except @hashed' do + expect_list_of_orphans(%w(orphan1 orphan2)) + + subject.multi_check + end + end + + context 'few orphans with existing namespace' do + let!(:first_level) { create(:group, path: 'my-namespace') } + let(:disk_namespaces) { %w(/repos/orphan1 /repos/orphan2 /repos/my-namespace /repos/@hashed) } + + it 'prints list of orphaned namespaces' do + expect_list_of_orphans(%w(orphan1 orphan2)) + + subject.multi_check + end + end + + context 'few orphans with existing namespace and parents with same name as orphans' do + let!(:first_level) { create(:group, path: 'my-namespace') } + let!(:second_level) { create(:group, path: 'second-level', parent: first_level) } + let(:disk_namespaces) { %w(/repos/orphan1 /repos/orphan2 /repos/my-namespace /repos/second-level /repos/@hashed) } + + it 'prints list of orphaned namespaces ignoring parents with same namespace as orphans' do + expect_list_of_orphans(%w(orphan1 orphan2 second-level)) + + subject.multi_check + end + end + + context 'no orphans' do + let(:disk_namespaces) { %w(@hashed) } + + it 'prints an empty list ignoring @hashed' do + expect_list_of_orphans([]) + + subject.multi_check + end + end + end + + def expect_list_of_orphans(orphans) + expect(subject).to receive(:print_orphans).with(orphans, 'default') + end +end diff --git a/spec/lib/system_check/orphans/repository_check_spec.rb b/spec/lib/system_check/orphans/repository_check_spec.rb new file mode 100644 index 00000000000..b0c2267d177 --- /dev/null +++ b/spec/lib/system_check/orphans/repository_check_spec.rb @@ -0,0 +1,68 @@ +require 'spec_helper' +require 'rake_helper' + +describe SystemCheck::Orphans::RepositoryCheck do + let(:storages) { Gitlab.config.repositories.storages.reject { |key, _| key.eql? 'broken' } } + + before do + allow(Gitlab.config.repositories).to receive(:storages).and_return(storages) + allow(subject).to receive(:fetch_disk_namespaces).and_return(disk_namespaces) + allow(subject).to receive(:fetch_disk_repositories).and_return(disk_repositories) + # silence_output + end + + describe '#multi_check' do + context 'all orphans' do + let(:disk_namespaces) { %w(/repos/orphan1 /repos/orphan2 repos/@hashed) } + let(:disk_repositories) { %w(repo1.git repo2.git) } + + it 'prints list of all orphaned namespaces except @hashed' do + expect_list_of_orphans(%w(orphan1/repo1.git orphan1/repo2.git orphan2/repo1.git orphan2/repo2.git)) + + subject.multi_check + end + end + + context 'few orphans with existing namespace' do + let!(:first_level) { create(:group, path: 'my-namespace') } + let!(:project) { create(:project, path: 'repo', namespace: first_level) } + let(:disk_namespaces) { %w(/repos/orphan1 /repos/orphan2 /repos/my-namespace /repos/@hashed) } + let(:disk_repositories) { %w(repo.git) } + + it 'prints list of orphaned namespaces' do + expect_list_of_orphans(%w(orphan1/repo.git orphan2/repo.git)) + + subject.multi_check + end + end + + context 'few orphans with existing namespace and parents with same name as orphans' do + let!(:first_level) { create(:group, path: 'my-namespace') } + let!(:second_level) { create(:group, path: 'second-level', parent: first_level) } + let!(:project) { create(:project, path: 'repo', namespace: first_level) } + let(:disk_namespaces) { %w(/repos/orphan1 /repos/orphan2 /repos/my-namespace /repos/second-level /repos/@hashed) } + let(:disk_repositories) { %w(repo.git) } + + it 'prints list of orphaned namespaces ignoring parents with same namespace as orphans' do + expect_list_of_orphans(%w(orphan1/repo.git orphan2/repo.git second-level/repo.git)) + + subject.multi_check + end + end + + context 'no orphans' do + let(:disk_namespaces) { %w(@hashed) } + let(:disk_repositories) { %w(repo.git) } + + it 'prints an empty list ignoring @hashed' do + expect_list_of_orphans([]) + + subject.multi_check + end + end + end + + def expect_list_of_orphans(orphans) + expect(subject).to receive(:print_orphans).with(orphans, 'default') + end +end diff --git a/spec/migrations/convert_custom_notification_settings_to_columns_spec.rb b/spec/migrations/convert_custom_notification_settings_to_columns_spec.rb index 1396d12e5a9..759e77ac9db 100644 --- a/spec/migrations/convert_custom_notification_settings_to_columns_spec.rb +++ b/spec/migrations/convert_custom_notification_settings_to_columns_spec.rb @@ -2,6 +2,8 @@ require 'spec_helper' require Rails.root.join('db', 'post_migrate', '20170607121233_convert_custom_notification_settings_to_columns') describe ConvertCustomNotificationSettingsToColumns, :migration do + let(:user_class) { table(:users) } + let(:settings_params) do [ { level: 0, events: [:new_note] }, # disabled, single event @@ -19,7 +21,7 @@ describe ConvertCustomNotificationSettingsToColumns, :migration do events[event] = true end - user = create(:user) + user = build(:user).becomes(user_class).tap(&:save!) create_params = { user_id: user.id, level: params[:level], events: events } notification_setting = described_class::NotificationSetting.create(create_params) @@ -35,7 +37,7 @@ describe ConvertCustomNotificationSettingsToColumns, :migration do events[event] = true end - user = create(:user) + user = build(:user).becomes(user_class).tap(&:save!) create_params = events.merge(user_id: user.id, level: params[:level]) notification_setting = described_class::NotificationSetting.create(create_params) diff --git a/spec/models/abuse_report_spec.rb b/spec/models/abuse_report_spec.rb index d4da30b1641..f49a61062c1 100644 --- a/spec/models/abuse_report_spec.rb +++ b/spec/models/abuse_report_spec.rb @@ -1,8 +1,9 @@ require 'rails_helper' -RSpec.describe AbuseReport do - subject { create(:abuse_report) } - let(:user) { create(:admin) } +describe AbuseReport do + set(:report) { create(:abuse_report) } + set(:user) { create(:admin) } + subject { report } it { expect(subject).to be_valid } diff --git a/spec/models/appearance_spec.rb b/spec/models/appearance_spec.rb index b5d5d58697b..49f44525b29 100644 --- a/spec/models/appearance_spec.rb +++ b/spec/models/appearance_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -RSpec.describe Appearance do +describe Appearance do subject { build(:appearance) } it { is_expected.to be_valid } diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index c7a9eabdf06..78cacf9ff5d 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -167,19 +167,33 @@ describe ApplicationSetting do context 'housekeeping settings' do it { is_expected.not_to allow_value(0).for(:housekeeping_incremental_repack_period) } - it 'wants the full repack period to be longer than the incremental repack period' do + it 'wants the full repack period to be at least the incremental repack period' do subject.housekeeping_incremental_repack_period = 2 subject.housekeeping_full_repack_period = 1 expect(subject).not_to be_valid end - it 'wants the gc period to be longer than the full repack period' do - subject.housekeeping_full_repack_period = 2 - subject.housekeeping_gc_period = 1 + it 'wants the gc period to be at least the full repack period' do + subject.housekeeping_full_repack_period = 100 + subject.housekeeping_gc_period = 90 expect(subject).not_to be_valid end + + it 'allows the same period for incremental repack and full repack, effectively skipping incremental repack' do + subject.housekeeping_incremental_repack_period = 2 + subject.housekeeping_full_repack_period = 2 + + expect(subject).to be_valid + end + + it 'allows the same period for full repack and gc, effectively skipping full repack' do + subject.housekeeping_full_repack_period = 100 + subject.housekeeping_gc_period = 100 + + expect(subject).to be_valid + end end end diff --git a/spec/models/chat_name_spec.rb b/spec/models/chat_name_spec.rb index 8581bcbb08b..e89e534d914 100644 --- a/spec/models/chat_name_spec.rb +++ b/spec/models/chat_name_spec.rb @@ -1,7 +1,8 @@ require 'spec_helper' describe ChatName do - subject { create(:chat_name) } + set(:chat_name) { create(:chat_name) } + subject { chat_name } it { is_expected.to belong_to(:service) } it { is_expected.to belong_to(:user) } diff --git a/spec/models/chat_team_spec.rb b/spec/models/chat_team_spec.rb index e0e5f73e6fe..70a9a206faa 100644 --- a/spec/models/chat_team_spec.rb +++ b/spec/models/chat_team_spec.rb @@ -1,7 +1,8 @@ require 'spec_helper' describe ChatTeam do - subject { create(:chat_team) } + set(:chat_team) { create(:chat_team) } + subject { chat_team } # Associations it { is_expected.to belong_to(:namespace) } diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index c2c9f1c12d1..451968c7342 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -1,18 +1,19 @@ require 'spec_helper' describe Ci::Build do - let(:user) { create(:user) } - let(:project) { create(:project, :repository) } - let(:build) { create(:ci_build, pipeline: pipeline) } - let(:test_trace) { 'This is a test' } + set(:user) { create(:user) } + set(:group) { create(:group, :access_requestable) } + set(:project) { create(:project, :repository, group: group) } - let(:pipeline) do + set(:pipeline) do create(:ci_pipeline, project: project, sha: project.commit.id, ref: project.default_branch, status: 'success') end + let(:build) { create(:ci_build, pipeline: pipeline) } + it { is_expected.to belong_to(:runner) } it { is_expected.to belong_to(:trigger_request) } it { is_expected.to belong_to(:erased_by) } @@ -282,7 +283,7 @@ describe Ci::Build do let(:project_regex) { '\(\d+\.\d+\) covered' } before do - project.build_coverage_regex = project_regex + project.update_column(:build_coverage_regex, project_regex) end context 'and coverage_regex attribute is not set' do @@ -1096,9 +1097,6 @@ describe Ci::Build do end describe '#repo_url' do - let(:build) { create(:ci_build) } - let(:project) { build.project } - subject { build.repo_url } it { is_expected.to be_a(String) } @@ -1199,6 +1197,8 @@ describe Ci::Build do end context 'use from gitlab-ci.yml' do + let(:pipeline) { create(:ci_pipeline) } + before do stub_ci_pipeline_yaml_file(config) end @@ -1442,11 +1442,7 @@ describe Ci::Build do { key: 'SECRET_KEY', value: 'secret_value', public: false } end - let(:group) { create(:group, :access_requestable) } - before do - build.project.update(group: group) - create(:ci_group_variable, secret_variable.slice(:key, :value).merge(group: group)) end @@ -1459,11 +1455,7 @@ describe Ci::Build do { key: 'PROTECTED_KEY', value: 'protected_value', public: false } end - let(:group) { create(:group, :access_requestable) } - before do - build.project.update(group: group) - create(:ci_group_variable, :protected, protected_variable.slice(:key, :value).merge(group: group)) @@ -1486,6 +1478,10 @@ describe Ci::Build do end context 'when the ref is not protected' do + before do + build.update_column(:ref, 'some/feature') + end + it { is_expected.not_to include(protected_variable) } end end @@ -1552,6 +1548,8 @@ describe Ci::Build do end context 'when yaml_variables are undefined' do + let(:pipeline) { create(:ci_pipeline, project: project) } + before do build.yaml_variables = nil end @@ -1645,7 +1643,10 @@ describe Ci::Build do before do build.environment = 'production' - allow(project).to receive(:deployment_variables).and_return([deployment_variable]) + + allow_any_instance_of(Project) + .to receive(:deployment_variables) + .and_return([deployment_variable]) end it { is_expected.to include(deployment_variable) } @@ -1669,14 +1670,19 @@ describe Ci::Build do before do allow(build).to receive(:predefined_variables) { [build_pre_var] } - allow(project).to receive(:predefined_variables) { [project_pre_var] } - allow(pipeline).to receive(:predefined_variables) { [pipeline_pre_var] } allow(build).to receive(:yaml_variables) { [build_yaml_var] } - allow(project).to receive(:secret_variables_for) + allow_any_instance_of(Project) + .to receive(:predefined_variables) { [project_pre_var] } + + allow_any_instance_of(Project) + .to receive(:secret_variables_for) .with(ref: 'master', environment: nil) do [create(:ci_variable, key: 'secret', value: 'value')] end + + allow_any_instance_of(Ci::Pipeline) + .to receive(:predefined_variables) { [pipeline_pre_var] } end it do diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 95da97b7bc5..77f0be6b120 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -1439,4 +1439,24 @@ describe Ci::Pipeline, :mailer do it_behaves_like 'not sending any notification' end end + + describe '#latest_builds_with_artifacts' do + let!(:pipeline) { create(:ci_pipeline, :success) } + + let!(:build) do + create(:ci_build, :success, :artifacts, pipeline: pipeline) + end + + it 'returns the latest builds' do + expect(pipeline.latest_builds_with_artifacts).to eq([build]) + end + + it 'memoizes the returned relation' do + query_count = ActiveRecord::QueryRecorder + .new { 2.times { pipeline.latest_builds_with_artifacts.to_a } } + .count + + expect(query_count).to eq(1) + end + end end diff --git a/spec/models/gpg_key_spec.rb b/spec/models/gpg_key_spec.rb index 9c99c3e5c08..fadc8bfeb61 100644 --- a/spec/models/gpg_key_spec.rb +++ b/spec/models/gpg_key_spec.rb @@ -140,18 +140,6 @@ describe GpgKey do end end - describe 'notification', :mailer do - let(:user) { create(:user) } - - it 'sends a notification' do - perform_enqueued_jobs do - create(:gpg_key, user: user) - end - - should_email(user) - end - end - describe '#revoke' do it 'invalidates all associated gpg signatures and destroys the key' do gpg_key = create :gpg_key diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb index 96baeaff0a4..dbc4aba8547 100644 --- a/spec/models/key_spec.rb +++ b/spec/models/key_spec.rb @@ -169,16 +169,4 @@ describe Key, :mailer do expect(described_class.new(key: " #{valid_key} ").key).to eq(valid_key) end end - - describe 'notification' do - let(:user) { create(:user) } - - it 'sends a notification' do - perform_enqueued_jobs do - create(:key, user: user) - end - - should_email(user) - end - end end diff --git a/spec/models/lfs_objects_project_spec.rb b/spec/models/lfs_objects_project_spec.rb index d24d4cf7695..0a3180f43e8 100644 --- a/spec/models/lfs_objects_project_spec.rb +++ b/spec/models/lfs_objects_project_spec.rb @@ -1,8 +1,11 @@ require 'spec_helper' describe LfsObjectsProject do - subject { create(:lfs_objects_project, project: project) } - let(:project) { create(:project) } + set(:project) { create(:project) } + + subject do + create(:lfs_objects_project, project: project) + end describe 'associations' do it { is_expected.to belong_to(:project) } @@ -11,9 +14,13 @@ describe LfsObjectsProject do describe 'validation' do it { is_expected.to validate_presence_of(:lfs_object_id) } - it { is_expected.to validate_uniqueness_of(:lfs_object_id).scoped_to(:project_id).with_message("already exists in project") } - it { is_expected.to validate_presence_of(:project_id) } + + it 'validates object id' do + is_expected.to validate_uniqueness_of(:lfs_object_id) + .scoped_to(:project_id) + .with_message("already exists in project") + end end describe '#update_project_statistics' do diff --git a/spec/models/personal_access_token_spec.rb b/spec/models/personal_access_token_spec.rb index b2f2a3ce914..01440b15674 100644 --- a/spec/models/personal_access_token_spec.rb +++ b/spec/models/personal_access_token_spec.rb @@ -41,7 +41,7 @@ describe PersonalAccessToken do it 'revokes the token' do active_personal_access_token.revoke! - expect(active_personal_access_token.revoked?).to be true + expect(active_personal_access_token).to be_revoked end end @@ -61,10 +61,37 @@ describe PersonalAccessToken do expect(personal_access_token).to be_valid end - it "allows creating a token with read_registry scope" do - personal_access_token.scopes = [:read_registry] + context 'when registry is disabled' do + before do + stub_container_registry_config(enabled: false) + end - expect(personal_access_token).to be_valid + it "rejects creating a token with read_registry scope" do + personal_access_token.scopes = [:read_registry] + + expect(personal_access_token).not_to be_valid + expect(personal_access_token.errors[:scopes].first).to eq "can only contain available scopes" + end + + it "allows revoking a token with read_registry scope" do + personal_access_token.scopes = [:read_registry] + + personal_access_token.revoke! + + expect(personal_access_token).to be_revoked + end + end + + context 'when registry is enabled' do + before do + stub_container_registry_config(enabled: true) + end + + it "allows creating a token with read_registry scope" do + personal_access_token.scopes = [:read_registry] + + expect(personal_access_token).to be_valid + end end it "rejects creating a token with unavailable scopes" do diff --git a/spec/models/project_services/pipelines_email_service_spec.rb b/spec/models/project_services/pipelines_email_service_spec.rb index 5faab9ba38b..be07ca2d945 100644 --- a/spec/models/project_services/pipelines_email_service_spec.rb +++ b/spec/models/project_services/pipelines_email_service_spec.rb @@ -6,7 +6,8 @@ describe PipelinesEmailService, :mailer do end let(:project) { create(:project, :repository) } - let(:recipient) { 'test@gitlab.com' } + let(:recipients) { 'test@gitlab.com' } + let(:receivers) { [recipients] } let(:data) do Gitlab::DataBuilder::Pipeline.build(pipeline) @@ -48,18 +49,24 @@ describe PipelinesEmailService, :mailer do shared_examples 'sending email' do before do + subject.recipients = recipients + perform_enqueued_jobs do run end end it 'sends email' do - should_only_email(double(notification_email: recipient), kind: :bcc) + emails = receivers.map { |r| double(notification_email: r) } + + should_only_email(*emails, kind: :bcc) end end shared_examples 'not sending email' do before do + subject.recipients = recipients + perform_enqueued_jobs do run end @@ -75,10 +82,6 @@ describe PipelinesEmailService, :mailer do subject.test(data) end - before do - subject.recipients = recipient - end - context 'when pipeline is failed' do before do data[:object_attributes][:status] = 'failed' @@ -104,10 +107,6 @@ describe PipelinesEmailService, :mailer do end context 'with recipients' do - before do - subject.recipients = recipient - end - context 'with failed pipeline' do before do data[:object_attributes][:status] = 'failed' @@ -152,9 +151,7 @@ describe PipelinesEmailService, :mailer do end context 'with empty recipients list' do - before do - subject.recipients = ' ,, ' - end + let(:recipients) { ' ,, ' } context 'with failed pipeline' do before do @@ -165,5 +162,19 @@ describe PipelinesEmailService, :mailer do it_behaves_like 'not sending email' end end + + context 'with recipients list separating with newlines' do + let(:recipients) { "\ntest@gitlab.com, \r\nexample@gitlab.com" } + let(:receivers) { %w[test@gitlab.com example@gitlab.com] } + + context 'with failed pipeline' do + before do + data[:object_attributes][:status] = 'failed' + pipeline.update(status: 'failed') + end + + it_behaves_like 'sending email' + end + end end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 48fc77423ff..78226c6c3fa 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -2682,4 +2682,60 @@ describe Project do end end end + + describe '#latest_successful_builds_for' do + let(:project) { build(:project) } + + before do + allow(project).to receive(:default_branch).and_return('master') + end + + context 'without a ref' do + it 'returns a pipeline for the default branch' do + expect(project) + .to receive(:latest_successful_pipeline_for_default_branch) + + project.latest_successful_pipeline_for + end + end + + context 'with the ref set to the default branch' do + it 'returns a pipeline for the default branch' do + expect(project) + .to receive(:latest_successful_pipeline_for_default_branch) + + project.latest_successful_pipeline_for(project.default_branch) + end + end + + context 'with a ref that is not the default branch' do + it 'returns the latest successful pipeline for the given ref' do + expect(project.pipelines).to receive(:latest_successful_for).with('foo') + + project.latest_successful_pipeline_for('foo') + end + end + end + + describe '#latest_successful_pipeline_for_default_branch' do + let(:project) { build(:project) } + + before do + allow(project).to receive(:default_branch).and_return('master') + end + + it 'memoizes and returns the latest successful pipeline for the default branch' do + pipeline = double(:pipeline) + + expect(project.pipelines).to receive(:latest_successful_for) + .with(project.default_branch) + .and_return(pipeline) + .once + + 2.times do + expect(project.latest_successful_pipeline_for_default_branch) + .to eq(pipeline) + end + end + end end diff --git a/spec/models/push_event_spec.rb b/spec/models/push_event_spec.rb index 532fb024261..ad3c3a406d9 100644 --- a/spec/models/push_event_spec.rb +++ b/spec/models/push_event_spec.rb @@ -11,6 +11,94 @@ describe PushEvent do event end + describe '.created_or_pushed' do + let(:event1) { create(:push_event) } + let(:event2) { create(:push_event) } + let(:event3) { create(:push_event) } + + before do + create(:push_event_payload, event: event1, action: :pushed) + create(:push_event_payload, event: event2, action: :created) + create(:push_event_payload, event: event3, action: :removed) + end + + let(:relation) { described_class.created_or_pushed } + + it 'includes events for pushing to existing refs' do + expect(relation).to include(event1) + end + + it 'includes events for creating new refs' do + expect(relation).to include(event2) + end + + it 'does not include events for removing refs' do + expect(relation).not_to include(event3) + end + end + + describe '.branch_events' do + let(:event1) { create(:push_event) } + let(:event2) { create(:push_event) } + + before do + create(:push_event_payload, event: event1, ref_type: :branch) + create(:push_event_payload, event: event2, ref_type: :tag) + end + + let(:relation) { described_class.branch_events } + + it 'includes events for branches' do + expect(relation).to include(event1) + end + + it 'does not include events for tags' do + expect(relation).not_to include(event2) + end + end + + describe '.without_existing_merge_requests' do + let(:project) { create(:project, :repository) } + let(:event1) { create(:push_event, project: project) } + let(:event2) { create(:push_event, project: project) } + let(:event3) { create(:push_event, project: project) } + let(:event4) { create(:push_event, project: project) } + + before do + create(:push_event_payload, event: event1, ref: 'foo', action: :created) + create(:push_event_payload, event: event2, ref: 'bar', action: :created) + create(:push_event_payload, event: event3, ref: 'baz', action: :removed) + create(:push_event_payload, event: event4, ref: 'baz', ref_type: :tag) + + project.repository.create_branch('bar', 'master') + + create( + :merge_request, + source_project: project, + target_project: project, + source_branch: 'bar' + ) + end + + let(:relation) { described_class.without_existing_merge_requests } + + it 'includes events that do not have a corresponding merge request' do + expect(relation).to include(event1) + end + + it 'does not include events that have a corresponding merge request' do + expect(relation).not_to include(event2) + end + + it 'does not include events for removed refs' do + expect(relation).not_to include(event3) + end + + it 'does not include events for pushing to tags' do + expect(relation).not_to include(event4) + end + end + describe '.sti_name' do it 'returns Event::PUSHED' do expect(described_class.sti_name).to eq(Event::PUSHED) diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 7065d467ec0..53280f2c1cf 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -8,7 +8,7 @@ describe Repository, models: true do let(:repository) { project.repository } let(:broken_repository) { create(:project, :broken_storage).repository } let(:user) { create(:user) } - let(:committer) { Gitlab::Git::Committer.from_user(user) } + let(:git_user) { Gitlab::Git::User.from_gitlab(user) } let(:commit_options) do author = repository.user_to_committer(user) @@ -886,7 +886,7 @@ describe Repository, models: true do context 'when pre hooks were successful' do it 'runs without errors' do expect_any_instance_of(Gitlab::Git::HooksService).to receive(:execute) - .with(committer, repository.raw_repository, old_rev, blank_sha, 'refs/heads/feature') + .with(git_user, repository.raw_repository, old_rev, blank_sha, 'refs/heads/feature') expect { repository.rm_branch(user, 'feature') }.not_to raise_error end @@ -932,20 +932,20 @@ describe Repository, models: true do service = Gitlab::Git::HooksService.new expect(Gitlab::Git::HooksService).to receive(:new).and_return(service) expect(service).to receive(:execute) - .with(committer, target_repository.raw_repository, old_rev, new_rev, updating_ref) + .with(git_user, target_repository.raw_repository, old_rev, new_rev, updating_ref) .and_yield(service).and_return(true) end it 'runs without errors' do expect do - Gitlab::Git::OperationService.new(committer, repository.raw_repository).with_branch('feature') do + Gitlab::Git::OperationService.new(git_user, repository.raw_repository).with_branch('feature') do new_rev end end.not_to raise_error end it 'ensures the autocrlf Git option is set to :input' do - service = Gitlab::Git::OperationService.new(committer, repository.raw_repository) + service = Gitlab::Git::OperationService.new(git_user, repository.raw_repository) expect(service).to receive(:update_autocrlf_option) @@ -956,7 +956,7 @@ describe Repository, models: true do it 'updates the head' do expect(repository.find_branch('feature').dereferenced_target.id).to eq(old_rev) - Gitlab::Git::OperationService.new(committer, repository.raw_repository).with_branch('feature') do + Gitlab::Git::OperationService.new(git_user, repository.raw_repository).with_branch('feature') do new_rev end @@ -974,7 +974,7 @@ describe Repository, models: true do expect(target_project.repository.raw_repository).to receive(:fetch_ref) .and_call_original - Gitlab::Git::OperationService.new(committer, target_repository.raw_repository) + Gitlab::Git::OperationService.new(git_user, target_repository.raw_repository) .with_branch( 'master', start_repository: project.repository.raw_repository, @@ -990,7 +990,7 @@ describe Repository, models: true do it 'does not fetch_ref and just pass the commit' do expect(target_repository).not_to receive(:fetch_ref) - Gitlab::Git::OperationService.new(committer, target_repository.raw_repository) + Gitlab::Git::OperationService.new(git_user, target_repository.raw_repository) .with_branch('feature', start_repository: project.repository.raw_repository) { new_rev } end end @@ -1009,7 +1009,7 @@ describe Repository, models: true do end expect do - Gitlab::Git::OperationService.new(committer, target_project.repository.raw_repository) + Gitlab::Git::OperationService.new(git_user, target_project.repository.raw_repository) .with_branch('feature', start_repository: project.repository.raw_repository, &:itself) @@ -1031,7 +1031,7 @@ describe Repository, models: true do repository.add_branch(user, branch, old_rev) expect do - Gitlab::Git::OperationService.new(committer, repository.raw_repository).with_branch(branch) do + Gitlab::Git::OperationService.new(git_user, repository.raw_repository).with_branch(branch) do new_rev end end.not_to raise_error @@ -1049,7 +1049,7 @@ describe Repository, models: true do # Updating 'master' to new_rev would lose the commits on 'master' that # are not contained in new_rev. This should not be allowed. expect do - Gitlab::Git::OperationService.new(committer, repository.raw_repository).with_branch(branch) do + Gitlab::Git::OperationService.new(git_user, repository.raw_repository).with_branch(branch) do new_rev end end.to raise_error(Gitlab::Git::CommitError) @@ -1061,7 +1061,7 @@ describe Repository, models: true do allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, '']) expect do - Gitlab::Git::OperationService.new(committer, repository.raw_repository).with_branch('feature') do + Gitlab::Git::OperationService.new(git_user, repository.raw_repository).with_branch('feature') do new_rev end end.to raise_error(Gitlab::Git::HooksService::PreReceiveError) diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 36d1f6f3644..c1affa812aa 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -716,6 +716,7 @@ describe User do it "applies defaults to user" do expect(user.projects_limit).to eq(Gitlab.config.gitlab.default_projects_limit) expect(user.can_create_group).to eq(Gitlab.config.gitlab.default_can_create_group) + expect(user.theme_id).to eq(Gitlab.config.gitlab.default_theme) expect(user.external).to be_falsey end end @@ -726,6 +727,7 @@ describe User do it "applies defaults to user" do expect(user.projects_limit).to eq(123) expect(user.can_create_group).to be_falsey + expect(user.theme_id).to eq(1) end end @@ -1347,56 +1349,24 @@ describe User do end describe "#recent_push" do - subject { create(:user) } - let!(:project1) { create(:project, :repository) } - let!(:project2) { create(:project, :repository, forked_from_project: project1) } - - let!(:push_event) do - event = create(:push_event, project: project2, author: subject) - - create(:push_event_payload, - event: event, - commit_to: '1cf19a015df3523caf0a1f9d40c98a267d6a2fc2', - commit_count: 0, - ref: 'master') - - event - end - - before do - project1.team << [subject, :master] - project2.team << [subject, :master] - end - - it "includes push event" do - expect(subject.recent_push).to eq(push_event) - end - - it "excludes push event if branch has been deleted" do - allow_any_instance_of(Repository).to receive(:branch_exists?).with('master').and_return(false) - - expect(subject.recent_push).to eq(nil) - end + let(:user) { build(:user) } + let(:project) { build(:project) } + let(:event) { build(:push_event) } - it "excludes push event if MR is opened for it" do - create(:merge_request, source_project: project2, target_project: project1, source_branch: project2.default_branch, target_branch: 'fix', author: subject) + it 'returns the last push event for the user' do + expect_any_instance_of(Users::LastPushEventService) + .to receive(:last_event_for_user) + .and_return(event) - expect(subject.recent_push).to eq(nil) + expect(user.recent_push).to eq(event) end - it "includes push events on any of the provided projects" do - expect(subject.recent_push(project1)).to eq(nil) - expect(subject.recent_push(project2)).to eq(push_event) - - push_event1 = create(:push_event, project: project1, author: subject) - - create(:push_event_payload, - event: push_event1, - commit_to: '1cf19a015df3523caf0a1f9d40c98a267d6a2fc2', - commit_count: 0, - ref: 'master') + it 'returns the last push event for a project when one is given' do + expect_any_instance_of(Users::LastPushEventService) + .to receive(:last_event_for_project) + .and_return(event) - expect(subject.recent_push([project1, project2])).to eq(push_event1) # Newest + expect(user.recent_push(project)).to eq(event) end end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 9602584f546..92e7d797cbd 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -54,9 +54,9 @@ describe API::Projects do shared_examples_for 'projects response without N + 1 queries' do it 'avoids N + 1 queries' do - control_count = ActiveRecord::QueryRecorder.new do + control = ActiveRecord::QueryRecorder.new do get api('/projects', current_user) - end.count + end if defined?(additional_project) additional_project @@ -66,7 +66,7 @@ describe API::Projects do expect do get api('/projects', current_user) - end.not_to exceed_query_limit(control_count + 8) + end.not_to exceed_query_limit(control).with_threshold(8) end end diff --git a/spec/requests/api/services_spec.rb b/spec/requests/api/services_spec.rb index 48d99841385..7e174903918 100644 --- a/spec/requests/api/services_spec.rb +++ b/spec/requests/api/services_spec.rb @@ -1,10 +1,13 @@ require "spec_helper" describe API::Services do - let(:user) { create(:user) } - let(:admin) { create(:admin) } - let(:user2) { create(:user) } - let(:project) { create(:project, creator_id: user.id, namespace: user.namespace) } + set(:user) { create(:user) } + set(:admin) { create(:admin) } + set(:user2) { create(:user) } + + set(:project) do + create(:project, creator_id: user.id, namespace: user.namespace) + end Service.available_services_names.each do |service| describe "PUT /projects/:id/services/#{service.dasherize}" do @@ -98,8 +101,6 @@ describe API::Services do end describe 'POST /projects/:id/services/:slug/trigger' do - let!(:project) { create(:project) } - describe 'Mattermost Service' do let(:service_name) { 'mattermost_slash_commands' } diff --git a/spec/requests/jwt_controller_spec.rb b/spec/requests/jwt_controller_spec.rb index 8d79ea3dd40..41bf43a9bce 100644 --- a/spec/requests/jwt_controller_spec.rb +++ b/spec/requests/jwt_controller_spec.rb @@ -49,6 +49,10 @@ describe JwtController do let(:pat) { create(:personal_access_token, user: user, scopes: ['read_registry']) } let(:headers) { { authorization: credentials('personal_access_token', pat.token) } } + before do + stub_container_registry_config(enabled: true) + end + subject! { get '/jwt/auth', parameters, headers } it 'authenticates correctly' do diff --git a/spec/services/ci/pipeline_trigger_service_spec.rb b/spec/services/ci/pipeline_trigger_service_spec.rb index 9a6875e448c..f4ff818c479 100644 --- a/spec/services/ci/pipeline_trigger_service_spec.rb +++ b/spec/services/ci/pipeline_trigger_service_spec.rb @@ -34,6 +34,8 @@ describe Ci::PipelineTriggerService do expect(result[:pipeline].ref).to eq('master') expect(result[:pipeline].project).to eq(project) expect(result[:pipeline].user).to eq(trigger.owner) + expect(result[:pipeline].trigger_requests.to_a) + .to eq(result[:pipeline].builds.map(&:trigger_request).uniq) expect(result[:status]).to eq(:success) end diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb index bbc3a8c79f5..fbb3213f42b 100644 --- a/spec/services/ci/retry_build_service_spec.rb +++ b/spec/services/ci/retry_build_service_spec.rb @@ -1,9 +1,10 @@ require 'spec_helper' describe Ci::RetryBuildService do - let(:user) { create(:user) } - let(:project) { create(:project) } - let(:pipeline) { create(:ci_pipeline, project: project) } + set(:user) { create(:user) } + set(:project) { create(:project) } + set(:pipeline) { create(:ci_pipeline, project: project) } + let(:build) { create(:ci_build, pipeline: pipeline) } let(:service) do @@ -37,7 +38,7 @@ describe Ci::RetryBuildService do :queued, :coverage, :tags, :allowed_to_fail, :on_tag, :triggered, :trace, :teardown_environment, description: 'my-job', stage: 'test', pipeline: pipeline, - auto_canceled_by: create(:ci_empty_pipeline)) do |build| + auto_canceled_by: create(:ci_empty_pipeline, project: project)) do |build| ## # TODO, workaround for FactoryGirl limitation when having both # stage (text) and stage_id (integer) columns in the table. diff --git a/spec/services/deploy_keys/create_service_spec.rb b/spec/services/deploy_keys/create_service_spec.rb new file mode 100644 index 00000000000..7a604c0cadd --- /dev/null +++ b/spec/services/deploy_keys/create_service_spec.rb @@ -0,0 +1,12 @@ +require 'spec_helper' + +describe DeployKeys::CreateService do + let(:user) { create(:user) } + let(:params) { attributes_for(:deploy_key) } + + subject { described_class.new(user, params) } + + it "creates a deploy key" do + expect { subject.execute }.to change { DeployKey.where(params.merge(user: user)).count }.by(1) + end +end diff --git a/spec/services/event_create_service_spec.rb b/spec/services/event_create_service_spec.rb index 02d7ddeb86b..13395a7cac3 100644 --- a/spec/services/event_create_service_spec.rb +++ b/spec/services/event_create_service_spec.rb @@ -149,6 +149,14 @@ describe EventCreateService do .to change { user_activity(user) } end + it 'caches the last push event for the user' do + expect_any_instance_of(Users::LastPushEventService) + .to receive(:cache_last_push_event) + .with(an_instance_of(PushEvent)) + + service.push(project, user, push_data) + end + it 'does not create any event data when an error is raised' do payload_service = double(:service) diff --git a/spec/services/gpg_keys/create_service_spec.rb b/spec/services/gpg_keys/create_service_spec.rb new file mode 100644 index 00000000000..20382a3a618 --- /dev/null +++ b/spec/services/gpg_keys/create_service_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe GpgKeys::CreateService do + let(:user) { create(:user) } + let(:params) { attributes_for(:gpg_key) } + + subject { described_class.new(user, params) } + + context 'notification', :mailer do + it 'sends a notification' do + perform_enqueued_jobs do + subject.execute + end + should_email(user) + end + end + + it 'creates a gpg key' do + expect { subject.execute }.to change { user.gpg_keys.where(params).count }.by(1) + end +end diff --git a/spec/services/keys/create_service_spec.rb b/spec/services/keys/create_service_spec.rb new file mode 100644 index 00000000000..bcb436c1e46 --- /dev/null +++ b/spec/services/keys/create_service_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe Keys::CreateService do + let(:user) { create(:user) } + let(:params) { attributes_for(:key) } + + subject { described_class.new(user, params) } + + context 'notification', :mailer do + it 'sends a notification' do + perform_enqueued_jobs do + subject.execute + end + should_email(user) + end + end + + it 'creates a key' do + expect { subject.execute }.to change { user.keys.where(params).count }.by(1) + end +end diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index 3e493148b32..f4b36eb7eeb 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -84,7 +84,6 @@ describe NotificationService, :mailer do let!(:key) { create(:personal_key, key_options) } it { expect(notification.new_key(key)).to be_truthy } - it { should_email(key.user) } describe 'never emails the ghost user' do let(:key_options) { { user: User.ghost } } diff --git a/spec/services/projects/housekeeping_service_spec.rb b/spec/services/projects/housekeeping_service_spec.rb index 437c009e7fa..b7b5de07380 100644 --- a/spec/services/projects/housekeeping_service_spec.rb +++ b/spec/services/projects/housekeeping_service_spec.rb @@ -75,7 +75,7 @@ describe Projects::HousekeepingService do end end - it 'uses all three kinds of housekeeping we offer' do + it 'goes through all three housekeeping tasks, executing only the highest task when there is overlap' do allow(subject).to receive(:try_obtain_lease).and_return(:the_uuid) allow(subject).to receive(:lease_key).and_return(:the_lease_key) diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb index 92cc9a37795..c551083ac90 100644 --- a/spec/services/projects/update_service_spec.rb +++ b/spec/services/projects/update_service_spec.rb @@ -57,6 +57,21 @@ describe Projects::UpdateService, '#execute' do end end end + + context 'When project visibility is higher than parent group' do + let(:group) { create(:group, visibility_level: Gitlab::VisibilityLevel::INTERNAL) } + + before do + project.update(namespace: group, visibility_level: group.visibility_level) + end + + it 'does not update project visibility level' do + result = update_project(project, admin, visibility_level: Gitlab::VisibilityLevel::PUBLIC) + + expect(result).to eq({ status: :error, message: 'Visibility level public is not allowed in a internal group.' }) + expect(project.reload).to be_internal + end + end end describe 'when updating project that has forks' do @@ -148,7 +163,7 @@ describe Projects::UpdateService, '#execute' do result = update_project(project, admin, path: 'existing') expect(result).to include(status: :error) - expect(result[:message]).to match('Project could not be updated!') + expect(result[:message]).to match('There is already a repository with that name on disk') expect(project).not_to be_valid expect(project.errors.messages).to have_key(:base) expect(project.errors.messages[:base]).to include('There is already a repository with that name on disk') @@ -159,8 +174,10 @@ describe Projects::UpdateService, '#execute' do it 'returns an error result when record cannot be updated' do result = update_project(project, admin, { name: 'foo&bar' }) - expect(result).to eq({ status: :error, - message: 'Project could not be updated!' }) + expect(result).to eq({ + status: :error, + message: "Name can contain only letters, digits, emojis, '_', '.', dash, space. It must start with letter, digit, emoji or '_'." + }) end end diff --git a/spec/services/users/last_push_event_service_spec.rb b/spec/services/users/last_push_event_service_spec.rb new file mode 100644 index 00000000000..956358738fe --- /dev/null +++ b/spec/services/users/last_push_event_service_spec.rb @@ -0,0 +1,112 @@ +require 'spec_helper' + +describe Users::LastPushEventService do + let(:user) { build(:user, id: 1) } + let(:project) { build(:project, id: 2) } + let(:event) { build(:push_event, id: 3, author: user, project: project) } + let(:service) { described_class.new(user) } + + describe '#cache_last_push_event' do + it "caches the event for the event's project and current user" do + expect(service).to receive(:set_key) + .ordered + .with('last-push-event/1/2', 3) + + expect(service).to receive(:set_key) + .ordered + .with('last-push-event/1', 3) + + service.cache_last_push_event(event) + end + + it 'caches the event for the origin project when pushing to a fork' do + source = build(:project, id: 5) + + allow(project).to receive(:forked?).and_return(true) + allow(project).to receive(:forked_from_project).and_return(source) + + expect(service).to receive(:set_key) + .ordered + .with('last-push-event/1/2', 3) + + expect(service).to receive(:set_key) + .ordered + .with('last-push-event/1', 3) + + expect(service).to receive(:set_key) + .ordered + .with('last-push-event/1/5', 3) + + service.cache_last_push_event(event) + end + end + + describe '#last_event_for_user' do + it 'returns the last push event for the current user' do + expect(service).to receive(:find_cached_event) + .with('last-push-event/1') + .and_return(event) + + expect(service.last_event_for_user).to eq(event) + end + + it 'returns nil when no push event could be found' do + expect(service).to receive(:find_cached_event) + .with('last-push-event/1') + .and_return(nil) + + expect(service.last_event_for_user).to be_nil + end + end + + describe '#last_event_for_project' do + it 'returns the last push event for the given project' do + expect(service).to receive(:find_cached_event) + .with('last-push-event/1/2') + .and_return(event) + + expect(service.last_event_for_project(project)).to eq(event) + end + + it 'returns nil when no push event could be found' do + expect(service).to receive(:find_cached_event) + .with('last-push-event/1/2') + .and_return(nil) + + expect(service.last_event_for_project(project)).to be_nil + end + end + + describe '#find_cached_event', :use_clean_rails_memory_store_caching do + context 'with a non-existing cache key' do + it 'returns nil' do + expect(service.find_cached_event('bla')).to be_nil + end + end + + context 'with an existing cache key' do + before do + service.cache_last_push_event(event) + end + + it 'returns a PushEvent when no merge requests exist for the event' do + allow(service).to receive(:find_event_in_database) + .with(event.id) + .and_return(event) + + expect(service.find_cached_event('last-push-event/1')).to eq(event) + end + + it 'removes the cache key when no event could be found and returns nil' do + allow(PushEvent).to receive(:without_existing_merge_requests) + .and_return(PushEvent.none) + + expect(Rails.cache).to receive(:delete) + .with('last-push-event/1') + .and_call_original + + expect(service.find_cached_event('last-push-event/1')).to be_nil + end + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index ff1754fbe7e..92735336572 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,7 +1,7 @@ require './spec/simplecov_env' SimpleCovEnv.start! -ENV["RAILS_ENV"] ||= 'test' +ENV["RAILS_ENV"] = 'test' ENV["IN_MEMORY_APPLICATION_SETTINGS"] = 'true' require File.expand_path("../../config/environment", __FILE__) diff --git a/spec/support/api/scopes/read_user_shared_examples.rb b/spec/support/api/scopes/read_user_shared_examples.rb index 3bd589d64b9..57e28e040d7 100644 --- a/spec/support/api/scopes/read_user_shared_examples.rb +++ b/spec/support/api/scopes/read_user_shared_examples.rb @@ -23,6 +23,10 @@ shared_examples_for 'allows the "read_user" scope' do context 'when the requesting token does not have any required scope' do let(:token) { create(:personal_access_token, scopes: ['read_registry'], user: user) } + before do + stub_container_registry_config(enabled: true) + end + it 'returns a "401" response' do get api_call.call(path, user, personal_access_token: token) diff --git a/spec/support/gitlab_stubs/session.json b/spec/support/gitlab_stubs/session.json index cd55d63125e..688175369ae 100644 --- a/spec/support/gitlab_stubs/session.json +++ b/spec/support/gitlab_stubs/session.json @@ -7,7 +7,7 @@ "skype":"aertert", "linkedin":"", "twitter":"", - "color_scheme_id":2, + "theme_id":2,"color_scheme_id":2, "state":"active", "created_at":"2012-12-21T13:02:20Z", "extern_uid":null, diff --git a/spec/support/gitlab_stubs/user.json b/spec/support/gitlab_stubs/user.json index cd55d63125e..ce8dfe5ae75 100644 --- a/spec/support/gitlab_stubs/user.json +++ b/spec/support/gitlab_stubs/user.json @@ -7,7 +7,7 @@ "skype":"aertert", "linkedin":"", "twitter":"", - "color_scheme_id":2, + "theme_id":2,"color_scheme_id":2, "state":"active", "created_at":"2012-12-21T13:02:20Z", "extern_uid":null, @@ -17,4 +17,4 @@ "can_create_project":false, "private_token":"Wvjy2Krpb7y8xi93owUz", "access_token":"Wvjy2Krpb7y8xi93owUz" -} +}
\ No newline at end of file diff --git a/spec/support/migrations_helpers.rb b/spec/support/migrations_helpers.rb index 4ca019c1b05..6522d74ba89 100644 --- a/spec/support/migrations_helpers.rb +++ b/spec/support/migrations_helpers.rb @@ -16,7 +16,9 @@ module MigrationsHelpers end def reset_column_in_migration_models - ActiveRecord::Base.clear_cache! + ActiveRecord::Base.connection_pool.connections.each do |conn| + conn.schema_cache.clear! + end described_class.constants.sort.each do |name| const = described_class.const_get(name) diff --git a/spec/support/query_recorder.rb b/spec/support/query_recorder.rb index 55b531b4cf7..ba0b805caad 100644 --- a/spec/support/query_recorder.rb +++ b/spec/support/query_recorder.rb @@ -34,15 +34,47 @@ RSpec::Matchers.define :exceed_query_limit do |expected| supports_block_expectations match do |block| - query_count(&block) > expected + query_count(&block) > expected_count + threshold end failure_message_when_negated do |actual| - "Expected a maximum of #{expected} queries, got #{@recorder.count}:\n\n#{@recorder.log_message}" + threshold_message = threshold > 0 ? " (+#{@threshold})" : '' + counts = "#{expected_count}#{threshold_message}" + "Expected a maximum of #{counts} queries, got #{actual_count}:\n\n#{log_message}" + end + + def with_threshold(threshold) + @threshold = threshold + self + end + + def threshold + @threshold.to_i + end + + def expected_count + if expected.is_a?(ActiveRecord::QueryRecorder) + expected.count + else + expected + end + end + + def actual_count + @recorder.count end def query_count(&block) @recorder = ActiveRecord::QueryRecorder.new(&block) @recorder.count end + + def log_message + if expected.is_a?(ActiveRecord::QueryRecorder) + extra_queries = (expected.log - @recorder.log).join("\n\n") + "Extra queries: \n\n #{extra_queries}" + else + @recorder.log_message + end + end end diff --git a/spec/support/shared_examples/features/issuables_user_dropdown_behaviors_shared_examples.rb b/spec/support/shared_examples/features/issuables_user_dropdown_behaviors_shared_examples.rb new file mode 100644 index 00000000000..c92c7f603d6 --- /dev/null +++ b/spec/support/shared_examples/features/issuables_user_dropdown_behaviors_shared_examples.rb @@ -0,0 +1,21 @@ +shared_examples 'issuable user dropdown behaviors' do + include FilteredSearchHelpers + + before do + issuable # ensure we have at least one issuable + sign_in(user_in_dropdown) + end + + %w[author assignee].each do |dropdown| + describe "#{dropdown} dropdown", :js do + it 'only includes members of the project/group' do + visit issuables_path + + filtered_search.set("#{dropdown}:") + + expect(find("#js-dropdown-#{dropdown} .filter-dropdown")).to have_content(user_in_dropdown.name) + expect(find("#js-dropdown-#{dropdown} .filter-dropdown")).not_to have_content(user_not_in_dropdown.name) + end + end + end +end diff --git a/spec/support/project_features_apply_to_issuables_shared_examples.rb b/spec/support/shared_examples/features/project_features_apply_to_issuables_shared_examples.rb index 639b0924197..639b0924197 100644 --- a/spec/support/project_features_apply_to_issuables_shared_examples.rb +++ b/spec/support/shared_examples/features/project_features_apply_to_issuables_shared_examples.rb diff --git a/spec/support/stub_gitlab_calls.rb b/spec/support/stub_gitlab_calls.rb index 78a2ff73746..9695f35bd25 100644 --- a/spec/support/stub_gitlab_calls.rb +++ b/spec/support/stub_gitlab_calls.rb @@ -26,9 +26,11 @@ module StubGitlabCalls end def stub_container_registry_config(registry_settings) - allow(Gitlab.config.registry).to receive_messages(registry_settings) allow(Auth::ContainerRegistryAuthenticationService) .to receive(:full_access_token).and_return('token') + + allow(Gitlab.config.registry).to receive_messages(registry_settings) + load 'lib/gitlab/auth.rb' end def stub_container_registry_tags(repository: :any, tags:) diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index 71b9deeabc3..6e5b9700b54 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -3,6 +3,8 @@ require 'rspec/mocks' module TestEnv extend self + ComponentFailedToInstallError = Class.new(StandardError) + # When developing the seed repository, comment out the branch you will modify. BRANCH_SHA = { 'signed-commits' => '2d1096e', @@ -63,6 +65,11 @@ module TestEnv # See gitlab.yml.example test section for paths # def init(opts = {}) + unless Rails.env.test? + puts "\nTestEnv.init can only be run if `RAILS_ENV` is set to 'test' not '#{Rails.env}'!\n" + exit 1 + end + # Disable mailer for spinach tests disable_mailer if opts[:mailer] == false @@ -122,50 +129,23 @@ module TestEnv end def setup_gitlab_shell - puts "\n==> Setting up Gitlab Shell..." - start = Time.now - gitlab_shell_dir = Gitlab.config.gitlab_shell.path - shell_needs_update = component_needs_update?(gitlab_shell_dir, - Gitlab::Shell.version_required) - - unless !shell_needs_update || system('rake', 'gitlab:shell:install') - puts "\nGitLab Shell failed to install, cleaning up #{gitlab_shell_dir}!\n" - FileUtils.rm_rf(gitlab_shell_dir) - exit 1 - end - - puts " GitLab Shell setup in #{Time.now - start} seconds...\n" + component_timed_setup('GitLab Shell', + install_dir: Gitlab.config.gitlab_shell.path, + version: Gitlab::Shell.version_required, + task: 'gitlab:shell:install') end def setup_gitaly - puts "\n==> Setting up Gitaly..." - start = Time.now socket_path = Gitlab::GitalyClient.address('default').sub(/\Aunix:/, '') gitaly_dir = File.dirname(socket_path) - if gitaly_dir_stale?(gitaly_dir) - puts " Gitaly is outdated, cleaning up #{gitaly_dir}!" - FileUtils.rm_rf(gitaly_dir) - end - - gitaly_needs_update = component_needs_update?(gitaly_dir, - Gitlab::GitalyClient.expected_server_version) + component_timed_setup('Gitaly', + install_dir: gitaly_dir, + version: Gitlab::GitalyClient.expected_server_version, + task: "gitlab:gitaly:install[#{gitaly_dir}]") do - unless !gitaly_needs_update || system('rake', "gitlab:gitaly:install[#{gitaly_dir}]") - puts "\nGitaly failed to install, cleaning up #{gitaly_dir}!\n" - FileUtils.rm_rf(gitaly_dir) - exit 1 + start_gitaly(gitaly_dir) end - - start_gitaly(gitaly_dir) - puts " Gitaly setup in #{Time.now - start} seconds...\n" - end - - def gitaly_dir_stale?(dir) - gitaly_executable = File.join(dir, 'gitaly') - return false unless File.exist?(gitaly_executable) - - File.mtime(gitaly_executable) < File.mtime(Rails.root.join('GITALY_SERVER_VERSION')) end def start_gitaly(gitaly_dir) @@ -320,6 +300,40 @@ module TestEnv end end + def component_timed_setup(component, install_dir:, version:, task:) + puts "\n==> Setting up #{component}..." + start = Time.now + + ensure_component_dir_name_is_correct!(component, install_dir) + + if component_needs_update?(install_dir, version) + # Cleanup the component entirely to ensure we start fresh + FileUtils.rm_rf(install_dir) + unless system('rake', task) + raise ComponentFailedToInstallError + end + end + + yield if block_given? + + rescue ComponentFailedToInstallError + puts "\n#{component} failed to install, cleaning up #{install_dir}!\n" + FileUtils.rm_rf(install_dir) + exit 1 + ensure + puts " #{component} setup in #{Time.now - start} seconds...\n" + end + + def ensure_component_dir_name_is_correct!(component, path) + actual_component_dir_name = File.basename(path) + expected_component_dir_name = component.parameterize + + unless actual_component_dir_name == expected_component_dir_name + puts " #{component} install dir should be named '#{expected_component_dir_name}', not '#{actual_component_dir_name}' (full install path given was '#{path}')!\n" + exit 1 + end + end + def component_needs_update?(component_folder, expected_version) version = File.read(File.join(component_folder, 'VERSION')).strip diff --git a/spec/views/ci/lints/show.html.haml_spec.rb b/spec/views/ci/lints/show.html.haml_spec.rb index f2c19c7642a..7724d54c569 100644 --- a/spec/views/ci/lints/show.html.haml_spec.rb +++ b/spec/views/ci/lints/show.html.haml_spec.rb @@ -4,7 +4,7 @@ describe 'ci/lints/show' do include Devise::Test::ControllerHelpers describe 'XSS protection' do - let(:config_processor) { Ci::GitlabCiYamlProcessor.new(YAML.dump(content)) } + let(:config_processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(content)) } before do assign(:status, true) assign(:builds, config_processor.builds) @@ -59,7 +59,7 @@ describe 'ci/lints/show' do } end - let(:config_processor) { Ci::GitlabCiYamlProcessor.new(YAML.dump(content)) } + let(:config_processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(content)) } context 'when the content is valid' do before do diff --git a/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb index b17bc6692f3..c5f455b8948 100644 --- a/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb +++ b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb @@ -1,16 +1,28 @@ require 'spec_helper' describe 'layouts/nav/sidebar/_project' do + let(:project) { create(:project, :repository) } + + before do + assign(:project, project) + assign(:repository, project.repository) + allow(view).to receive(:current_ref).and_return('master') + + allow(view).to receive(:can?).and_return(true) + end + + describe 'issue boards' do + it 'has boards tab when multiple issue boards available' do + render + + expect(rendered).to have_css('a[title="Board"]') + end + end + describe 'container registry tab' do before do - project = create(:project, :repository) stub_container_registry_config(enabled: true) - assign(:project, project) - assign(:repository, project.repository) - allow(view).to receive(:current_ref).and_return('master') - - allow(view).to receive(:can?).and_return(true) allow(controller).to receive(:controller_name) .and_return('repositories') allow(controller).to receive(:controller_path) diff --git a/spec/workers/git_garbage_collect_worker_spec.rb b/spec/workers/git_garbage_collect_worker_spec.rb index 6f9ddb6c63c..f7b67b8efc6 100644 --- a/spec/workers/git_garbage_collect_worker_spec.rb +++ b/spec/workers/git_garbage_collect_worker_spec.rb @@ -31,8 +31,8 @@ describe GitGarbageCollectWorker do expect_any_instance_of(Repository).to receive(:after_create_branch).and_call_original expect_any_instance_of(Repository).to receive(:branch_names).and_call_original + expect_any_instance_of(Repository).to receive(:has_visible_content?).and_call_original expect_any_instance_of(Gitlab::Git::Repository).to receive(:branch_count).and_call_original - expect_any_instance_of(Gitlab::Git::Repository).to receive(:has_visible_content?).and_call_original subject.perform(project.id, :gc, lease_key, lease_uuid) end @@ -77,8 +77,8 @@ describe GitGarbageCollectWorker do expect_any_instance_of(Repository).to receive(:after_create_branch).and_call_original expect_any_instance_of(Repository).to receive(:branch_names).and_call_original + expect_any_instance_of(Repository).to receive(:has_visible_content?).and_call_original expect_any_instance_of(Gitlab::Git::Repository).to receive(:branch_count).and_call_original - expect_any_instance_of(Gitlab::Git::Repository).to receive(:has_visible_content?).and_call_original subject.perform(project.id) end |