diff options
author | Bob Van Landuyt <bob@vanlanduyt.co> | 2018-09-06 16:09:30 +0300 |
---|---|---|
committer | Bob Van Landuyt <bob@vanlanduyt.co> | 2018-09-06 16:09:30 +0300 |
commit | f28940e6c488f012642f3e3dc696d2023215f6fb (patch) | |
tree | bea7e1514d1a8faffb37078671283d5dd41c482d | |
parent | 6038ca1864ea36f01c57620e3c4ef75c708e3a60 (diff) | |
parent | bac9a1dde1e75d057e212ab5d3b9b478d3981e1d (diff) |
Merge branch 'master' into 11-3-stable-prepare-rc4
251 files changed, 4744 insertions, 1110 deletions
diff --git a/.gitlab/CODEOWNERS b/.gitlab/CODEOWNERS new file mode 100644 index 00000000000..5b6e5a719fa --- /dev/null +++ b/.gitlab/CODEOWNERS @@ -0,0 +1,15 @@ +# Backend Maintainers are the default for all ruby files +*.rb @ayufan @DouweM @dzaporozhets @grzesiek @nick.thomas @rspeicher @rymai @smcgivern +*.rake @ayufan @DouweM @dzaporozhets @grzesiek @nick.thomas @rspeicher @rymai @smcgivern + +# Technical writing team are the default reviewers for everything in `doc/` +/doc/ @axil @marcia + +# Frontend maintainers should see everything in `app/assets/` +app/assets/ @annabeldunstone @ClemMakesApps @fatihacet @filipa @iamphill @mikegreiling @timzallmann + +# Someone from the database team should review changes in `db/` +db/ @abrandl @NikolayS + +# Feature specific owners +/ee/lib/gitlab/code_owners/ @reprazent diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION index 09b254e90c6..dfda3e0b4f0 100644 --- a/GITLAB_WORKHORSE_VERSION +++ b/GITLAB_WORKHORSE_VERSION @@ -1 +1 @@ -6.0.0 +6.1.0 @@ -68,7 +68,7 @@ gem 'u2f', '~> 0.2.1' gem 'validates_hostname', '~> 1.0.6' # Browser detection -gem 'browser', '~> 2.2' +gem 'browser', '~> 2.5' # GPG gem 'gpgme' @@ -107,7 +107,9 @@ gem 'kaminari', '~> 1.0' gem 'hamlit', '~> 2.8.8' # Files attachments -gem 'carrierwave', '~> 1.2' +# Locked until https://github.com/carrierwaveuploader/carrierwave/pull/2332/files is merged. +# config/initializers/carrierwave_patch.rb can be removed once that change is released. +gem 'carrierwave', '= 1.2.3' gem 'mini_magick' # Drag and Drop UI diff --git a/Gemfile.lock b/Gemfile.lock index 91cd360e708..e7ab97fb299 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -90,7 +90,7 @@ GEM msgpack (~> 1.0) bootstrap_form (2.7.0) brakeman (4.2.1) - browser (2.2.0) + browser (2.5.3) builder (3.2.3) bullet (5.5.1) activesupport (>= 3.0.0) @@ -991,12 +991,12 @@ DEPENDENCIES bootsnap (~> 1.3) bootstrap_form (~> 2.7.0) brakeman (~> 4.2) - browser (~> 2.2) + browser (~> 2.5) bullet (~> 5.5.0) bundler-audit (~> 0.5.0) capybara (~> 2.15) capybara-screenshot (~> 1.0.0) - carrierwave (~> 1.2) + carrierwave (= 1.2.3) charlock_holmes (~> 0.7.5) chronic (~> 0.10.2) chronic_duration (~> 0.10.6) diff --git a/Gemfile.rails5.lock b/Gemfile.rails5.lock index ba9b06a08cb..1a1aac9f439 100644 --- a/Gemfile.rails5.lock +++ b/Gemfile.rails5.lock @@ -93,7 +93,7 @@ GEM msgpack (~> 1.0) bootstrap_form (2.7.0) brakeman (4.2.1) - browser (2.2.0) + browser (2.5.3) builder (3.2.3) bullet (5.5.1) activesupport (>= 3.0.0) @@ -1000,12 +1000,12 @@ DEPENDENCIES bootsnap (~> 1.3) bootstrap_form (~> 2.7.0) brakeman (~> 4.2) - browser (~> 2.2) + browser (~> 2.5) bullet (~> 5.5.0) bundler-audit (~> 0.5.0) capybara (~> 2.15) capybara-screenshot (~> 1.0.0) - carrierwave (~> 1.2) + carrierwave (= 1.2.3) charlock_holmes (~> 0.7.5) chronic (~> 0.10.2) chronic_duration (~> 0.10.6) diff --git a/app/assets/images/auth_buttons/auth0_64.png b/app/assets/images/auth_buttons/auth0_64.png Binary files differnew file mode 100644 index 00000000000..5ad59659380 --- /dev/null +++ b/app/assets/images/auth_buttons/auth0_64.png diff --git a/app/assets/images/auth_buttons/azure_64.png b/app/assets/images/auth_buttons/azure_64.png Binary files differindex 85de7793440..168a9c81395 100644 --- a/app/assets/images/auth_buttons/azure_64.png +++ b/app/assets/images/auth_buttons/azure_64.png diff --git a/app/assets/images/auth_buttons/bitbucket_64.png b/app/assets/images/auth_buttons/bitbucket_64.png Binary files differindex b3d022a5a70..0edf7f52a11 100644 --- a/app/assets/images/auth_buttons/bitbucket_64.png +++ b/app/assets/images/auth_buttons/bitbucket_64.png diff --git a/app/assets/images/auth_buttons/google_64.png b/app/assets/images/auth_buttons/google_64.png Binary files differindex 720824230a5..389c1cd54ca 100644 --- a/app/assets/images/auth_buttons/google_64.png +++ b/app/assets/images/auth_buttons/google_64.png diff --git a/app/assets/images/auth_buttons/jwt_64.png b/app/assets/images/auth_buttons/jwt_64.png Binary files differnew file mode 100644 index 00000000000..ca97ae47002 --- /dev/null +++ b/app/assets/images/auth_buttons/jwt_64.png diff --git a/app/assets/images/auth_buttons/shibboleth_64.png b/app/assets/images/auth_buttons/shibboleth_64.png Binary files differnew file mode 100644 index 00000000000..d4c752f9400 --- /dev/null +++ b/app/assets/images/auth_buttons/shibboleth_64.png diff --git a/app/assets/javascripts/diffs/components/changed_files_dropdown.vue b/app/assets/javascripts/diffs/components/changed_files_dropdown.vue index 045688a32bf..0ec6b8b7f21 100644 --- a/app/assets/javascripts/diffs/components/changed_files_dropdown.vue +++ b/app/assets/javascripts/diffs/components/changed_files_dropdown.vue @@ -63,7 +63,7 @@ export default { v-else role="button" class="fa fa-times dropdown-input-search" - @click="clearSearch" + @click.stop.prevent="clearSearch" ></i> </div> <div class="dropdown-content"> diff --git a/app/assets/javascripts/groups/components/groups.vue b/app/assets/javascripts/groups/components/groups.vue index adde8c8cdb3..73ae928b0d9 100644 --- a/app/assets/javascripts/groups/components/groups.vue +++ b/app/assets/javascripts/groups/components/groups.vue @@ -37,7 +37,7 @@ </script> <template> - <div class="groups-list-tree-container"> + <div class="groups-list-tree-container qa-groups-list-tree-container"> <div v-if="searchEmpty" class="has-no-search-results" diff --git a/app/assets/javascripts/pages/admin/application_settings/index.js b/app/assets/javascripts/pages/admin/application_settings/index.js index 47bd70537f1..069f8ce55d0 100644 --- a/app/assets/javascripts/pages/admin/application_settings/index.js +++ b/app/assets/javascripts/pages/admin/application_settings/index.js @@ -1,8 +1,13 @@ import initSettingsPanels from '~/settings_panels'; import projectSelect from '~/project_select'; +import UsagePingPayload from './usage_ping_payload'; document.addEventListener('DOMContentLoaded', () => { // Initialize expandable settings panels initSettingsPanels(); projectSelect(); + new UsagePingPayload( + document.querySelector('.js-usage-ping-payload-trigger'), + document.querySelector('.js-usage-ping-payload'), + ).init(); }); diff --git a/app/assets/javascripts/pages/admin/application_settings/usage_ping_payload.js b/app/assets/javascripts/pages/admin/application_settings/usage_ping_payload.js new file mode 100644 index 00000000000..9a1bc46bf4a --- /dev/null +++ b/app/assets/javascripts/pages/admin/application_settings/usage_ping_payload.js @@ -0,0 +1,62 @@ +import axios from '../../../lib/utils/axios_utils'; +import { __ } from '../../../locale'; +import flash from '../../../flash'; + +export default class UsagePingPayload { + constructor(trigger, container) { + this.trigger = trigger; + this.container = container; + this.isVisible = false; + this.isInserted = false; + } + + init() { + this.spinner = this.trigger.querySelector('.js-spinner'); + this.text = this.trigger.querySelector('.js-text'); + + this.trigger.addEventListener('click', event => { + event.preventDefault(); + + if (this.isVisible) return this.hidePayload(); + + return this.requestPayload(); + }); + } + + requestPayload() { + if (this.isInserted) return this.showPayload(); + + this.spinner.classList.add('d-inline'); + + return axios + .get(this.container.dataset.endpoint, { + responseType: 'text', + }) + .then(({ data }) => { + this.spinner.classList.remove('d-inline'); + this.insertPayload(data); + }) + .catch(() => { + this.spinner.classList.remove('d-inline'); + flash(__('Error fetching usage ping data.')); + }); + } + + hidePayload() { + this.isVisible = false; + this.container.classList.add('d-none'); + this.text.textContent = __('Preview payload'); + } + + showPayload() { + this.isVisible = true; + this.container.classList.remove('d-none'); + this.text.textContent = __('Hide payload'); + } + + insertPayload(data) { + this.isInserted = true; + this.container.innerHTML = data; + this.showPayload(); + } +} diff --git a/app/assets/javascripts/pages/instance_statistics/cohorts/index.js b/app/assets/javascripts/pages/instance_statistics/cohorts/index.js deleted file mode 100644 index 2d5020dbef4..00000000000 --- a/app/assets/javascripts/pages/instance_statistics/cohorts/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import initUsagePing from './usage_ping'; - -document.addEventListener('DOMContentLoaded', initUsagePing); diff --git a/app/assets/javascripts/pages/instance_statistics/cohorts/usage_ping.js b/app/assets/javascripts/pages/instance_statistics/cohorts/usage_ping.js deleted file mode 100644 index 914a9661c27..00000000000 --- a/app/assets/javascripts/pages/instance_statistics/cohorts/usage_ping.js +++ /dev/null @@ -1,13 +0,0 @@ -import axios from '../../../lib/utils/axios_utils'; -import { __ } from '../../../locale'; -import flash from '../../../flash'; - -export default function UsagePing() { - const el = document.querySelector('.usage-data'); - - axios.get(el.dataset.endpoint, { - responseType: 'text', - }).then(({ data }) => { - el.innerHTML = data; - }).catch(() => flash(__('Error fetching usage ping data.'))); -} diff --git a/app/assets/javascripts/pages/projects/project.js b/app/assets/javascripts/pages/projects/project.js index a853624e944..fdcbcc236c1 100644 --- a/app/assets/javascripts/pages/projects/project.js +++ b/app/assets/javascripts/pages/projects/project.js @@ -13,40 +13,52 @@ export default class Project { constructor() { const $cloneOptions = $('ul.clone-options-dropdown'); const $projectCloneField = $('#project_clone'); - const $cloneBtnText = $('a.clone-dropdown-btn span'); + const $cloneBtnLabel = $('.js-git-clone-holder .js-clone-dropdown-label'); - const selectedCloneOption = $cloneBtnText.text().trim(); + const selectedCloneOption = $cloneBtnLabel.text().trim(); if (selectedCloneOption.length > 0) { $(`a:contains('${selectedCloneOption}')`, $cloneOptions).addClass('is-active'); } - $('a', $cloneOptions).on('click', (e) => { + $('a', $cloneOptions).on('click', e => { + e.preventDefault(); const $this = $(e.currentTarget); const url = $this.attr('href'); - const activeText = $this.find('.dropdown-menu-inner-title').text(); + const cloneType = $this.data('cloneType'); - e.preventDefault(); + $('.is-active', $cloneOptions).removeClass('is-active'); + $(`a[data-clone-type="${cloneType}"]`).each(function() { + const $el = $(this); + const activeText = $el.find('.dropdown-menu-inner-title').text(); + const $container = $el.closest('.project-clone-holder'); + const $label = $container.find('.js-clone-dropdown-label'); - $('.is-active', $cloneOptions).not($this).removeClass('is-active'); - $this.toggleClass('is-active'); - $projectCloneField.val(url); - $cloneBtnText.text(activeText); + $el.toggleClass('is-active'); + $label.text(activeText); + }); - return $('.clone').text(url); + $projectCloneField.val(url); + $('.js-git-empty .js-clone').text(url); }); // Ref switcher Project.initRefSwitcher(); $('.project-refs-select').on('change', function() { - return $(this).parents('form').submit(); + return $(this) + .parents('form') + .submit(); }); $('.hide-no-ssh-message').on('click', function(e) { Cookies.set('hide_no_ssh_message', 'false'); - $(this).parents('.no-ssh-key-message').remove(); + $(this) + .parents('.no-ssh-key-message') + .remove(); return e.preventDefault(); }); $('.hide-no-password-message').on('click', function(e) { Cookies.set('hide_no_password_message', 'false'); - $(this).parents('.no-password-message').remove(); + $(this) + .parents('.no-password-message') + .remove(); return e.preventDefault(); }); Project.projectSelectDropdown(); @@ -58,7 +70,7 @@ export default class Project { } static changeProject(url) { - return window.location = url; + return (window.location = url); } static initRefSwitcher() { @@ -73,14 +85,15 @@ export default class Project { selected = $dropdown.data('selected'); return $dropdown.glDropdown({ data(term, callback) { - axios.get($dropdown.data('refsUrl'), { - params: { - ref: $dropdown.data('ref'), - search: term, - }, - }) - .then(({ data }) => callback(data)) - .catch(() => flash(__('An error occurred while getting projects'))); + axios + .get($dropdown.data('refsUrl'), { + params: { + ref: $dropdown.data('ref'), + search: term, + }, + }) + .then(({ data }) => callback(data)) + .catch(() => flash(__('An error occurred while getting projects'))); }, selectable: true, filterable: true, diff --git a/app/assets/javascripts/pages/projects/show/index.js b/app/assets/javascripts/pages/projects/show/index.js index b76f2f76449..0507f67843f 100644 --- a/app/assets/javascripts/pages/projects/show/index.js +++ b/app/assets/javascripts/pages/projects/show/index.js @@ -8,15 +8,18 @@ import BlobViewer from '~/blob/viewer/index'; import Activities from '~/activities'; import { ajaxGet } from '~/lib/utils/common_utils'; import GpgBadges from '~/gpg_badges'; +import initReadMore from '~/read_more'; import Star from '../../../star'; import notificationsDropdown from '../../../notifications_dropdown'; document.addEventListener('DOMContentLoaded', () => { + initReadMore(); new Star(); // eslint-disable-line no-new notificationsDropdown(); new ShortcutsNavigation(); // eslint-disable-line no-new new NotificationsForm(); // eslint-disable-line no-new - new UserCallout({ // eslint-disable-line no-new + // eslint-disable-next-line no-new + new UserCallout({ setCalloutPerProject: false, className: 'js-autodevops-banner', }); diff --git a/app/assets/javascripts/read_more.js b/app/assets/javascripts/read_more.js new file mode 100644 index 00000000000..d2d1ac8c76a --- /dev/null +++ b/app/assets/javascripts/read_more.js @@ -0,0 +1,41 @@ +/** + * ReadMore + * + * Adds "read more" functionality to elements. + * + * Specifically, it looks for a trigger, by default ".js-read-more-trigger", and adds the class + * "is-expanded" to the previous element in order to provide a click to expand functionality. + * + * This is useful for long text elements that you would like to truncate, especially for mobile. + * + * Example Markup + * <div class="read-more-container"> + * <p>Some text that should be long enough to have to truncate within a specified container.</p> + * <p>This text will not appear in the container, as only the first line can be truncated.</p> + * <p>This should also not appear, if everything is working correctly!</p> + * </div> + * <button class="js-read-more-trigger">Read more</button> + * + */ +export default function initReadMore(triggerSelector = '.js-read-more-trigger') { + const triggerEls = document.querySelectorAll(triggerSelector); + + if (!triggerEls) return; + + triggerEls.forEach(triggerEl => { + const targetEl = triggerEl.previousElementSibling; + + if (!targetEl) { + return; + } + + triggerEl.addEventListener( + 'click', + e => { + targetEl.classList.add('is-expanded'); + e.target.remove(); + }, + { once: true }, + ); + }); +} diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index b1a20c06910..39ffabb3ea6 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -64,3 +64,4 @@ @import 'framework/ci_variable_list'; @import 'framework/feature_highlight'; @import 'framework/terms'; +@import 'framework/read_more'; diff --git a/app/assets/stylesheets/framework/awards.scss b/app/assets/stylesheets/framework/awards.scss index a265e4206f1..702276780e9 100644 --- a/app/assets/stylesheets/framework/awards.scss +++ b/app/assets/stylesheets/framework/awards.scss @@ -229,8 +229,8 @@ svg { margin-bottom: 1px; - height: 18px; - width: 18px; + height: $default-icon-size; + width: $default-icon-size; border-radius: 50%; path { diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index 72b4ed0ac33..e91e830fcac 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -149,7 +149,8 @@ &.btn-success, &.btn-new, &.btn-create, - &.btn-save { + &.btn-save, + &.btn-register { @include btn-green; } @@ -172,8 +173,7 @@ } &.btn-info, - &.btn-primary, - &.btn-register { + &.btn-primary { @include btn-blue; } @@ -248,7 +248,7 @@ .btn-terminal { svg { height: 14px; - width: 18px; + width: $default-icon-size; } } diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index abfe350677e..a52e6c4f6a7 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -216,8 +216,8 @@ vertical-align: inherit; img { - height: 18px; - width: 18px; + height: $default-icon-size; + width: $default-icon-size; } } diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss index 033e5e57177..6d20c46b99d 100644 --- a/app/assets/stylesheets/framework/mobile.scss +++ b/app/assets/stylesheets/framework/mobile.scss @@ -44,12 +44,8 @@ .project-repo-buttons { display: block; - .count-buttons .btn { - margin: 0 10px; - } - - .count-buttons .count-with-arrow { - display: none; + .count-buttons .count-badge { + margin-top: $gl-padding-8; } } } diff --git a/app/assets/stylesheets/framework/read_more.scss b/app/assets/stylesheets/framework/read_more.scss new file mode 100644 index 00000000000..b84b6e0b256 --- /dev/null +++ b/app/assets/stylesheets/framework/read_more.scss @@ -0,0 +1,13 @@ +.read-more-container { + @include media-breakpoint-down(md) { + &:not(.is-expanded) { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + + > * { + display: inline; + } + } + } +} diff --git a/app/assets/stylesheets/framework/toggle.scss b/app/assets/stylesheets/framework/toggle.scss index 20394cc1e52..43aaf198609 100644 --- a/app/assets/stylesheets/framework/toggle.scss +++ b/app/assets/stylesheets/framework/toggle.scss @@ -56,8 +56,8 @@ &, .toggle-icon-svg { - width: 18px; - height: 18px; + width: $default-icon-size; + height: $default-icon-size; } .toggle-icon-svg { diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index d76f5cbd9ff..f5e7a84d082 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -250,7 +250,7 @@ $container-text-max-width: 540px; $gl-avatar-size: 40px; $border-radius-default: 4px; $border-radius-small: 2px; -$settings-icon-size: 18px; +$default-icon-size: 18px; $layout-link-gray: #7e7c7c; $btn-side-margin: 10px; $btn-sm-side-margin: 7px; @@ -271,6 +271,7 @@ $performance-bar-height: 35px; $flash-height: 52px; $context-header-height: 60px; $breadcrumb-min-height: 48px; +$project-title-row-height: 24px; /* * Common component specific colors diff --git a/app/assets/stylesheets/pages/admin.scss b/app/assets/stylesheets/pages/admin.scss index 6c555aee20a..f0acb78f731 100644 --- a/app/assets/stylesheets/pages/admin.scss +++ b/app/assets/stylesheets/pages/admin.scss @@ -4,3 +4,7 @@ padding-bottom: 46px; } } + +.usage-data { + max-height: 400px; +} diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index 7d7143631f2..d673b59e1c0 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -749,6 +749,10 @@ left: $gl-padding; } + .dropdown-input .dropdown-input-search { + pointer-events: all; + } + .diff-changed-file { display: flex; padding-top: 8px; diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss index c9e5fb9c579..fa0ab1a3bae 100644 --- a/app/assets/stylesheets/pages/login.scss +++ b/app/assets/stylesheets/pages/login.scss @@ -100,6 +100,22 @@ p { margin: 0; } + + .omniauth-btn { + margin-bottom: $gl-padding; + width: 48%; + padding: $gl-padding-8; + + @include media-breakpoint-down(md) { + width: 100%; + } + + img { + width: $default-icon-size; + height: $default-icon-size; + margin-right: $gl-padding; + } + } } .new-session-tabs { @@ -169,10 +185,6 @@ } } - label { - font-weight: $gl-font-weight-normal; - } - .submit-container { margin-top: 16px; } @@ -200,15 +212,6 @@ } } -.oauth-image-link { - margin-right: 10px; - - img { - width: 32px; - height: 32px; - } -} - .devise-layout-html { margin: 0; padding: 0; diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index a95e78931b1..9b7051924e6 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -115,7 +115,7 @@ .project-feature-controls { display: flex; align-items: center; - margin: 8px 0; + margin: $gl-padding-8 0; max-width: 432px; .toggle-wrapper { @@ -144,12 +144,8 @@ .group-home-panel { padding-top: 24px; padding-bottom: 24px; + border-bottom: 1px solid $border-color; - @include media-breakpoint-up(sm) { - border-bottom: 1px solid $border-color; - } - - .project-avatar, .group-avatar { float: none; margin: 0 auto; @@ -175,7 +171,6 @@ } } - .project-home-desc, .group-home-desc { margin-left: auto; margin-right: auto; @@ -199,6 +194,62 @@ } } +.project-home-panel { + padding-top: $gl-padding-8; + padding-bottom: $gl-padding-24; + + .project-title-row { + margin-right: $gl-padding-8; + } + + .project-avatar { + width: $project-title-row-height; + height: $project-title-row-height; + flex-shrink: 0; + flex-basis: $project-title-row-height; + margin: 0 $gl-padding-8 0 0; + } + + .project-title { + font-size: 20px; + line-height: $project-title-row-height; + font-weight: bold; + } + + .project-metadata { + font-weight: normal; + font-size: 14px; + line-height: $gl-btn-line-height; + color: $gl-text-color-secondary; + + .icon { + margin-right: $gl-padding-4; + font-size: 16px; + } + + .project-visibility, + .project-license, + .project-tag-list { + margin-right: $gl-padding-8; + } + + .project-license { + .btn { + line-height: 0; + border-width: 0; + } + } + + .project-tag-list, + .project-license { + .icon { + position: relative; + top: 2px; + } + } + } +} + .nav > .project-repo-buttons { margin-top: 0; } @@ -206,8 +257,6 @@ .project-repo-buttons, .group-buttons { .btn { - padding: 3px 10px; - &:last-child { margin-left: 0; } @@ -222,11 +271,15 @@ .fa-caret-down { margin-left: 3px; + + &.dropdown-btn-icon { + margin-left: 0; + } } } .project-action-button { - margin: 15px 5px 0; + margin: $gl-padding $gl-padding-8 0 0; vertical-align: top; } @@ -243,82 +296,45 @@ .count-buttons { display: inline-block; vertical-align: top; - margin-top: 15px; - } + margin-top: $gl-padding; - .project-clone-holder { - display: inline-block; - margin: 15px 5px 0 0; + .count-badge { + height: $input-height; - input { - height: 28px; + .icon { + top: -1px; + } } - } - .count-with-arrow { - display: inline-block; - position: relative; - margin-left: 4px; + .count-badge-count, + .count-badge-button { + border: 1px solid $border-color; + line-height: 1; + } - .arrow { - &::before { - content: ''; - display: inline-block; - position: absolute; - width: 0; - height: 0; - border-color: transparent; - border-style: solid; - top: 50%; - left: 0; - margin-top: -6px; - border-width: 7px 5px 7px 0; - border-right-color: $count-arrow-border; - pointer-events: none; - } + .count, + .count-badge-button { + color: $gl-text-color; + } - &::after { - content: ''; - position: absolute; - width: 0; - height: 0; - border-color: transparent; - border-style: solid; - top: 50%; - left: 1px; - margin-top: -9px; - border-width: 10px 7px 10px 0; - border-right-color: $white-light; - pointer-events: none; - } + .count-badge-count { + padding: 0 12px; + border-right: 0; + border-radius: $border-radius-base 0 0 $border-radius-base; + background: $gray-light; } - .count { - @include btn-white; - display: inline-block; - background: $white-light; - border-radius: 2px; - border-width: 1px; - border-style: solid; - font-size: 13px; - font-weight: $gl-font-weight-bold; - line-height: 13px; - letter-spacing: 0.4px; - padding: 6px 14px; - text-align: center; - vertical-align: middle; - touch-action: manipulation; - background-image: none; - white-space: nowrap; - margin: 0 10px 0 4px; + .count-badge-button { + border-radius: 0 $border-radius-base $border-radius-base 0; + } + } - a { - color: inherit; - } + .project-clone-holder { + display: inline-block; + margin: $gl-padding $gl-padding-8 0 0; - &:hover { - background: $white-light; - } + input { + height: $input-height; } } @@ -333,6 +349,14 @@ min-width: 320px; } } + + .mobile-git-clone { + margin-top: $gl-padding-8; + + .dropdown-menu-inner-content { + @extend .monospace; + } + } } .split-one { @@ -511,7 +535,6 @@ .controls { margin-left: auto; } - } .choose-template { @@ -574,7 +597,7 @@ flex-wrap: wrap; .btn { - padding: 8px; + padding: $gl-padding-8; margin-right: 10px; } @@ -651,7 +674,7 @@ left: -10px; top: 50%; z-index: 10; - padding: 8px 0; + padding: $gl-padding-8 0; text-align: center; background-color: $white-light; color: $gl-text-color-tertiary; @@ -665,7 +688,7 @@ left: 50%; top: 0; transform: translateX(-50%); - padding: 0 8px; + padding: 0 $gl-padding-8; } } @@ -699,17 +722,51 @@ .project-stats { font-size: 0; text-align: center; - max-width: 100%; border-bottom: 1px solid $border-color; - .nav { - margin-top: $gl-padding-8; - margin-bottom: $gl-padding-8; + .scrolling-tabs-container { + .scrolling-tabs { + margin-top: $gl-padding-8; + margin-bottom: $gl-padding-8; + flex-wrap: wrap; + border-bottom: 0; + } + .fade-left, + .fade-right { + top: 0; + height: 100%; + + .fa { + top: 50%; + margin-top: -$gl-padding-8; + } + } + + .nav { + flex-basis: 100%; + + + .nav { + margin: $gl-padding-8 0; + } + } + + @include media-breakpoint-down(md) { + flex-direction: column; + + .nav { + flex-wrap: nowrap; + } + + .nav:first-child { + margin-right: $gl-padding-8; + } + } + } + + .nav { > li { display: inline-block; - margin-top: $gl-padding-4; - margin-bottom: $gl-padding-4; &:not(:last-child) { margin-right: $gl-padding; @@ -732,13 +789,17 @@ font-size: $gl-font-size; line-height: $gl-btn-line-height; color: $gl-text-color-secondary; + white-space: nowrap; } .stat-link { + border-bottom: 0; + &:hover, &:focus { color: $gl-text-color; text-decoration: underline; + border-bottom: 0; } } @@ -868,7 +929,7 @@ pre.light-well { } .git-clone-holder { - width: 380px; + width: 320px; .btn-clipboard { border: 1px solid $border-color; diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss index e351dd7c0bb..5a594920e44 100644 --- a/app/assets/stylesheets/pages/settings.scss +++ b/app/assets/stylesheets/pages/settings.scss @@ -106,7 +106,7 @@ .settings-list-icon { color: $gl-text-color-secondary; - font-size: $settings-icon-size; + font-size: $default-icon-size; line-height: 42px; } diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index e5b38898a67..7cd68d6b92a 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -110,6 +110,7 @@ class ApplicationController < ActionController::Base def append_info_to_payload(payload) super + payload[:ua] = request.env["HTTP_USER_AGENT"] payload[:remote_ip] = request.remote_ip logged_user = auth_user diff --git a/app/controllers/concerns/send_file_upload.rb b/app/controllers/concerns/send_file_upload.rb index 237c93daee8..382ec91f771 100644 --- a/app/controllers/concerns/send_file_upload.rb +++ b/app/controllers/concerns/send_file_upload.rb @@ -1,7 +1,11 @@ module SendFileUpload def send_upload(file_upload, send_params: {}, redirect_params: {}, attachment: nil, disposition: 'attachment') if attachment - redirect_params[:query] = { "response-content-disposition" => "#{disposition};filename=#{attachment.inspect}" } + # Response-Content-Type will not override an existing Content-Type in + # Google Cloud Storage, so the metadata needs to be cleared on GCS for + # this to work. However, this override works with AWS. + redirect_params[:query] = { "response-content-disposition" => "#{disposition};filename=#{attachment.inspect}", + "response-content-type" => guess_content_type(attachment) } # By default, Rails will send uploads with an extension of .js with a # content-type of text/javascript, which will trigger Rails' # cross-origin JavaScript protection. @@ -18,4 +22,14 @@ module SendFileUpload redirect_to file_upload.url(**redirect_params) end end + + def guess_content_type(filename) + types = MIME::Types.type_for(filename) + + if types.present? + types.first.content_type + else + "application/octet-stream" + end + end end diff --git a/app/controllers/instance_statistics/cohorts_controller.rb b/app/controllers/instance_statistics/cohorts_controller.rb index 7eba0a5ecdd..4b4e39db2e1 100644 --- a/app/controllers/instance_statistics/cohorts_controller.rb +++ b/app/controllers/instance_statistics/cohorts_controller.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class InstanceStatistics::CohortsController < InstanceStatistics::ApplicationController + before_action :authenticate_usage_ping_enabled_or_admin! + def index if Gitlab::CurrentSettings.usage_ping_enabled cohorts_results = Rails.cache.fetch('cohorts', expires_in: 1.day) do @@ -10,4 +12,8 @@ class InstanceStatistics::CohortsController < InstanceStatistics::ApplicationCon @cohorts = CohortsSerializer.new.represent(cohorts_results) end end + + def authenticate_usage_ping_enabled_or_admin! + render_404 unless Gitlab::CurrentSettings.usage_ping_enabled || current_user.admin? + end end diff --git a/app/controllers/projects/clusters_controller.rb b/app/controllers/projects/clusters_controller.rb index 358fe59618b..b4fd09c06e5 100644 --- a/app/controllers/projects/clusters_controller.rb +++ b/app/controllers/projects/clusters_controller.rb @@ -157,7 +157,8 @@ class Projects::ClustersController < Projects::ApplicationController :namespace, :api_url, :token, - :ca_cert + :ca_cert, + :authorization_type ]).merge( provider_type: :user, platform_type: :kubernetes diff --git a/app/controllers/projects/hooks_controller.rb b/app/controllers/projects/hooks_controller.rb index 2da2aad9b33..bbf8c7d5cbc 100644 --- a/app/controllers/projects/hooks_controller.rb +++ b/app/controllers/projects/hooks_controller.rb @@ -66,6 +66,7 @@ class Projects::HooksController < Projects::ApplicationController :enable_ssl_verification, :token, :url, + :push_events_branch_filter, *ProjectHook.triggers.values ) end diff --git a/app/finders/template_finder.rb b/app/finders/template_finder.rb new file mode 100644 index 00000000000..ea0251bffb6 --- /dev/null +++ b/app/finders/template_finder.rb @@ -0,0 +1,37 @@ +class TemplateFinder + VENDORED_TEMPLATES = { + dockerfiles: ::Gitlab::Template::DockerfileTemplate, + gitignores: ::Gitlab::Template::GitignoreTemplate, + gitlab_ci_ymls: ::Gitlab::Template::GitlabCiYmlTemplate + }.freeze + + class << self + def build(type, params = {}) + if type == :licenses + LicenseTemplateFinder.new(params) + else + new(type, params) + end + end + end + + attr_reader :type, :params + + attr_reader :vendored_templates + private :vendored_templates + + def initialize(type, params = {}) + @type = type + @params = params + + @vendored_templates = VENDORED_TEMPLATES.fetch(type) + end + + def execute + if params[:name] + vendored_templates.find(params[:name]) + else + vendored_templates.all + end + end +end diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index 00ebafd177b..96f7415ae98 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -158,32 +158,35 @@ module BlobHelper end def licenses_for_select - return @licenses_for_select if defined?(@licenses_for_select) - - grouped_licenses = LicenseTemplateFinder.new.execute.group_by(&:category) - categories = grouped_licenses.keys - - @licenses_for_select = categories.each_with_object({}) do |category, hash| - hash[category] = grouped_licenses[category].map do |license| - { name: license.name, id: license.id } - end - end + @licenses_for_select ||= template_dropdown_names(TemplateFinder.build(:licenses).execute) end def ref_project @ref_project ||= @target_project || @project end + def template_dropdown_names(items) + grouped = items.group_by(&:category) + categories = grouped.keys + + categories.each_with_object({}) do |category, hash| + hash[category] = grouped[category].map do |item| + { name: item.name, id: item.id } + end + end + end + private :template_dropdown_names + def gitignore_names - @gitignore_names ||= Gitlab::Template::GitignoreTemplate.dropdown_names + @gitignore_names ||= template_dropdown_names(TemplateFinder.build(:gitignores).execute) end def gitlab_ci_ymls - @gitlab_ci_ymls ||= Gitlab::Template::GitlabCiYmlTemplate.dropdown_names(params[:context]) + @gitlab_ci_ymls ||= template_dropdown_names(TemplateFinder.build(:gitlab_ci_ymls).execute) end def dockerfile_names - @dockerfile_names ||= Gitlab::Template::DockerfileTemplate.dropdown_names + @dockerfile_names ||= template_dropdown_names(TemplateFinder.build(:dockerfiles).execute) end def blob_editor_paths diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb index 26e3850a540..2b3fe57767c 100644 --- a/app/helpers/button_helper.rb +++ b/app/helpers/button_helper.rb @@ -61,7 +61,7 @@ module ButtonHelper dropdown_description = http_dropdown_description(protocol) append_url = project.http_url_to_repo if append_link - dropdown_item_with_description(protocol, dropdown_description, href: append_url) + dropdown_item_with_description(protocol, dropdown_description, href: append_url, data: { clone_type: 'http' }) end def http_dropdown_description(protocol) @@ -80,16 +80,17 @@ module ButtonHelper append_url = project.ssh_url_to_repo if append_link - dropdown_item_with_description('SSH', dropdown_description, href: append_url) + dropdown_item_with_description('SSH', dropdown_description, href: append_url, data: { clone_type: 'ssh' }) end - def dropdown_item_with_description(title, description, href: nil) + def dropdown_item_with_description(title, description, href: nil, data: nil) button_content = content_tag(:strong, title, class: 'dropdown-menu-inner-title') button_content << content_tag(:span, description, class: 'dropdown-menu-inner-content') if description content_tag (href ? :a : :span), (href ? button_content : title), class: "#{title.downcase}-selector", - href: (href if href) + href: (href if href), + data: (data if data) end end diff --git a/app/helpers/clusters_helper.rb b/app/helpers/clusters_helper.rb index 8fd0b6f14c6..73049c74d80 100644 --- a/app/helpers/clusters_helper.rb +++ b/app/helpers/clusters_helper.rb @@ -11,4 +11,8 @@ module ClustersHelper render 'projects/clusters/gcp_signup_offer_banner' end end + + def rbac_clusters_feature_enabled? + Feature.enabled?(:rbac_clusters) + end end diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb index a8a10c98d69..a5612372aa6 100644 --- a/app/helpers/icons_helper.rb +++ b/app/helpers/icons_helper.rb @@ -86,7 +86,7 @@ module IconsHelper end end - def visibility_level_icon(level, fw: true) + def visibility_level_icon(level, fw: true, options: {}) name = case level when Gitlab::VisibilityLevel::PRIVATE @@ -99,7 +99,7 @@ module IconsHelper name << " fw" if fw - icon(name) + icon(name, options) end def file_type_icon_class(type, mode, name) diff --git a/app/helpers/markup_helper.rb b/app/helpers/markup_helper.rb index cbb971cf8b7..3adaa1366c0 100644 --- a/app/helpers/markup_helper.rb +++ b/app/helpers/markup_helper.rb @@ -107,23 +107,23 @@ module MarkupHelper def markup(file_name, text, context = {}) context[:project] ||= @project - context[:markdown_engine] ||= :redcarpet + context[:markdown_engine] ||= :redcarpet unless commonmark_for_repositories_enabled? html = context.delete(:rendered) || markup_unsafe(file_name, text, context) prepare_for_rendering(html, context) end - def render_wiki_content(wiki_page) + def render_wiki_content(wiki_page, context = {}) text = wiki_page.content return '' unless text.present? - context = { + context.merge!( pipeline: :wiki, project: @project, project_wiki: @project_wiki, page_slug: wiki_page.slug, - issuable_state_filter_enabled: true, - markdown_engine: :redcarpet - } + issuable_state_filter_enabled: true + ) + context[:markdown_engine] ||= :redcarpet unless commonmark_for_repositories_enabled? html = case wiki_page.format @@ -178,6 +178,10 @@ module MarkupHelper end end + def commonmark_for_repositories_enabled? + Feature.enabled?(:commonmark_for_repositories, default_enabled: true) + end + private # Return +text+, truncated to +max_chars+ characters, excluding any HTML diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 18b3badda8d..80b45176a62 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -252,6 +252,10 @@ module ProjectsHelper "xcode://clone?repo=#{CGI.escape(default_url_to_repo(project))}" end + def legacy_render_context(params) + params[:legacy_render] ? { markdown_engine: :redcarpet } : {} + end + private def get_project_nav_tabs(project, current_user) @@ -351,6 +355,10 @@ module ProjectsHelper end end + def default_clone_label + _("Copy %{protocol} clone URL") % { protocol: default_clone_protocol.upcase } + end + def default_clone_protocol if allowed_protocols_present? enabled_protocol diff --git a/app/helpers/visibility_level_helper.rb b/app/helpers/visibility_level_helper.rb index cf2fe5a2019..7b64869c9ea 100644 --- a/app/helpers/visibility_level_helper.rb +++ b/app/helpers/visibility_level_helper.rb @@ -138,7 +138,7 @@ module VisibilityLevelHelper end def project_visibility_icon_description(level) - "#{visibility_level_label(level)} - #{project_visibility_level_description(level)}" + "#{project_visibility_level_description(level)}" end def visibility_level_label(level) diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb index 4853b23513c..93fc1b145b2 100644 --- a/app/models/ci/job_artifact.rb +++ b/app/models/ci/job_artifact.rb @@ -87,7 +87,9 @@ module Ci end def hashed_path? - super || self.try(:file_location).nil? + return true if trace? # ArchiveLegacyTraces background migration might not have `file_location` column + + super || self.file_location.nil? end def expire_in diff --git a/app/models/clusters/applications/helm.rb b/app/models/clusters/applications/helm.rb index 55bbf7cae7e..423071ec024 100644 --- a/app/models/clusters/applications/helm.rb +++ b/app/models/clusters/applications/helm.rb @@ -32,7 +32,8 @@ module Clusters def install_command Gitlab::Kubernetes::Helm::InitCommand.new( name: name, - files: files + files: files, + rbac: cluster.platform_kubernetes_rbac? ) end diff --git a/app/models/clusters/applications/ingress.rb b/app/models/clusters/applications/ingress.rb index 93f654e0638..bd0286ee3f9 100644 --- a/app/models/clusters/applications/ingress.rb +++ b/app/models/clusters/applications/ingress.rb @@ -39,6 +39,7 @@ module Clusters Gitlab::Kubernetes::Helm::InstallCommand.new( name: name, version: VERSION, + rbac: cluster.platform_kubernetes_rbac?, chart: chart, files: files ) diff --git a/app/models/clusters/applications/jupyter.rb b/app/models/clusters/applications/jupyter.rb index ef1c76c03bd..3d84eeed5a8 100644 --- a/app/models/clusters/applications/jupyter.rb +++ b/app/models/clusters/applications/jupyter.rb @@ -40,6 +40,7 @@ module Clusters Gitlab::Kubernetes::Helm::InstallCommand.new( name: name, version: VERSION, + rbac: cluster.platform_kubernetes_rbac?, chart: chart, files: files, repository: repository diff --git a/app/models/clusters/applications/prometheus.rb b/app/models/clusters/applications/prometheus.rb index 88399dbbb95..46d0388a464 100644 --- a/app/models/clusters/applications/prometheus.rb +++ b/app/models/clusters/applications/prometheus.rb @@ -48,6 +48,7 @@ module Clusters Gitlab::Kubernetes::Helm::InstallCommand.new( name: name, version: VERSION, + rbac: cluster.platform_kubernetes_rbac?, chart: chart, files: files ) @@ -71,7 +72,7 @@ module Clusters private def kube_client - cluster&.kubeclient + cluster&.kubeclient&.core_client end end end diff --git a/app/models/clusters/applications/runner.rb b/app/models/clusters/applications/runner.rb index bde255723c8..a4a2e2b79a6 100644 --- a/app/models/clusters/applications/runner.rb +++ b/app/models/clusters/applications/runner.rb @@ -33,6 +33,7 @@ module Clusters Gitlab::Kubernetes::Helm::InstallCommand.new( name: name, version: VERSION, + rbac: cluster.platform_kubernetes_rbac?, chart: chart, files: files, repository: repository diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb index 7cf75403ab6..d7011ef447a 100644 --- a/app/models/clusters/cluster.rb +++ b/app/models/clusters/cluster.rb @@ -42,6 +42,7 @@ module Clusters delegate :on_creation?, to: :provider, allow_nil: true delegate :active?, to: :platform_kubernetes, prefix: true, allow_nil: true + delegate :rbac?, to: :platform_kubernetes, prefix: true, allow_nil: true delegate :installed?, to: :application_helm, prefix: true, allow_nil: true delegate :installed?, to: :application_ingress, prefix: true, allow_nil: true diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb index e6ddca0d5d0..3a335909101 100644 --- a/app/models/clusters/platforms/kubernetes.rb +++ b/app/models/clusters/platforms/kubernetes.rb @@ -5,6 +5,7 @@ module Clusters class Kubernetes < ActiveRecord::Base include Gitlab::Kubernetes include ReactiveCaching + include EnumWithNil self.table_name = 'cluster_platforms_kubernetes' self.reactive_cache_key = ->(kubernetes) { [kubernetes.class.model_name.singular, kubernetes.id] } @@ -47,6 +48,12 @@ module Clusters alias_method :active?, :enabled? + enum_with_nil authorization_type: { + unknown_authorization: nil, + rbac: 1, + abac: 2 + } + def actual_namespace if namespace.present? namespace @@ -95,7 +102,7 @@ module Clusters end def kubeclient - @kubeclient ||= build_kubeclient! + @kubeclient ||= build_kube_client!(api_groups: ['api', 'apis/rbac.authorization.k8s.io']) end private @@ -115,15 +122,16 @@ module Clusters slug.gsub(/[^-a-z0-9]/, '-').gsub(/^-+/, '') end - def build_kubeclient!(api_path: 'api', api_version: 'v1') + def build_kube_client!(api_groups: ['api'], api_version: 'v1') raise "Incomplete settings" unless api_url && actual_namespace unless (username && password) || token raise "Either username/password or token is required to access API" end - ::Kubeclient::Client.new( - join_api_url(api_path), + Gitlab::Kubernetes::KubeClient.new( + api_url, + api_groups, api_version, auth_options: kubeclient_auth_options, ssl_options: kubeclient_ssl_options, @@ -133,7 +141,7 @@ module Clusters # Returns a hash of all pods in the namespace def read_pods - kubeclient = build_kubeclient! + kubeclient = build_kube_client! kubeclient.get_pods(namespace: actual_namespace).as_json rescue Kubeclient::HttpError => err @@ -157,15 +165,6 @@ module Clusters { bearer_token: token } end - def join_api_url(api_path) - url = URI.parse(api_url) - prefix = url.path.sub(%r{/+\z}, '') - - url.path = [prefix, api_path].join("/") - - url.to_s - end - def terminal_auth { token: token, diff --git a/app/models/concerns/case_sensitivity.rb b/app/models/concerns/case_sensitivity.rb index 6e80365ee5b..c93b6589ee7 100644 --- a/app/models/concerns/case_sensitivity.rb +++ b/app/models/concerns/case_sensitivity.rb @@ -9,23 +9,46 @@ module CaseSensitivity # # Unlike other ActiveRecord methods this method only operates on a Hash. def iwhere(params) - criteria = self - cast_lower = Gitlab::Database.postgresql? + criteria = self params.each do |key, value| - column = ActiveRecord::Base.connection.quote_table_name(key) + criteria = case value + when Array + criteria.where(value_in(key, value)) + else + criteria.where(value_equal(key, value)) + end + end + + criteria + end - condition = - if cast_lower - "LOWER(#{column}) = LOWER(:value)" - else - "#{column} = :value" - end + private + + def value_equal(column, value) + lower_value = lower_value(value) + + lower_column(arel_table[column]).eq(lower_value).to_sql + end - criteria = criteria.where(condition, value: value) + def value_in(column, values) + lower_values = values.map do |value| + lower_value(value) end - criteria + lower_column(arel_table[column]).in(lower_values).to_sql + end + + def lower_value(value) + return value if Gitlab::Database.mysql? + + Arel::Nodes::NamedFunction.new('LOWER', [Arel::Nodes.build_quoted(value)]) + end + + def lower_column(column) + return column if Gitlab::Database.mysql? + + column.lower end end end diff --git a/app/models/concerns/protected_ref.rb b/app/models/concerns/protected_ref.rb index e62e680af6e..af387c99f3d 100644 --- a/app/models/concerns/protected_ref.rb +++ b/app/models/concerns/protected_ref.rb @@ -50,14 +50,20 @@ module ProtectedRef .map(&:"#{action}_access_levels").flatten end + # Returns all protected refs that match the given ref name. + # This checks all records from the scope built up so far, and does + # _not_ return a relation. + # + # This method optionally takes in a list of `protected_refs` to search + # through, to avoid calling out to the database. def matching(ref_name, protected_refs: nil) - ProtectedRefMatcher.matching(self, ref_name, protected_refs: protected_refs) + (protected_refs || self.all).select { |protected_ref| protected_ref.matches?(ref_name) } end end private def ref_matcher - @ref_matcher ||= ProtectedRefMatcher.new(self) + @ref_matcher ||= RefMatcher.new(self.name) end end diff --git a/app/models/concerns/triggerable_hooks.rb b/app/models/concerns/triggerable_hooks.rb index 223a61119e5..c52baa0524c 100644 --- a/app/models/concerns/triggerable_hooks.rb +++ b/app/models/concerns/triggerable_hooks.rb @@ -29,6 +29,12 @@ module TriggerableHooks public_send(trigger) # rubocop:disable GitlabSecurity/PublicSend end + def select_active(hooks_scope, data) + select do |hook| + ActiveHookFilter.new(hook).matches?(hooks_scope, data) + end + end + private def triggerable_hooks(hooks) diff --git a/app/models/hooks/active_hook_filter.rb b/app/models/hooks/active_hook_filter.rb new file mode 100644 index 00000000000..ea046bea368 --- /dev/null +++ b/app/models/hooks/active_hook_filter.rb @@ -0,0 +1,14 @@ +class ActiveHookFilter + def initialize(hook) + @hook = hook + @push_events_filter_matcher = RefMatcher.new(@hook.push_events_branch_filter) + end + + def matches?(hooks_scope, data) + return true if hooks_scope != :push_hooks + return true if @hook.push_events_branch_filter.blank? + + branch_name = Gitlab::Git.branch_name(data[:ref]) + @push_events_filter_matcher.matches?(branch_name) + end +end diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb index f18aadefa5c..20f15c15277 100644 --- a/app/models/hooks/web_hook.rb +++ b/app/models/hooks/web_hook.rb @@ -9,6 +9,7 @@ class WebHook < ActiveRecord::Base allow_local_network: lambda(&:allow_local_requests?) } validates :token, format: { without: /\n/ } + validates :push_events_branch_filter, branch_filter: true def execute(data, hook_name) WebHookService.new(self, data, hook_name).execute diff --git a/app/models/project.rb b/app/models/project.rb index 67593c9b2fe..97d9fa355ef 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1184,10 +1184,9 @@ class Project < ActiveRecord::Base def execute_hooks(data, hooks_scope = :push_hooks) run_after_commit_or_now do - hooks.hooks_for(hooks_scope).each do |hook| + hooks.hooks_for(hooks_scope).select_active(hooks_scope, data).each do |hook| hook.async_execute(data, hooks_scope.to_s) end - SystemHooksService.new.execute_hooks(data, hooks_scope) end end diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb index bda1f67b8ff..f119555f16b 100644 --- a/app/models/project_services/kubernetes_service.rb +++ b/app/models/project_services/kubernetes_service.rb @@ -96,10 +96,10 @@ class KubernetesService < DeploymentService # Check we can connect to the Kubernetes API def test(*args) - kubeclient = build_kubeclient! + kubeclient = build_kube_client! - kubeclient.discover - { success: kubeclient.discovered, result: "Checked API discovery endpoint" } + kubeclient.core_client.discover + { success: kubeclient.core_client.discovered, result: "Checked API discovery endpoint" } rescue => err { success: false, result: err } end @@ -144,7 +144,7 @@ class KubernetesService < DeploymentService end def kubeclient - @kubeclient ||= build_kubeclient! + @kubeclient ||= build_kube_client!(api_groups: ['api', 'apis/rbac.authorization.k8s.io']) end def deprecated? @@ -182,11 +182,12 @@ class KubernetesService < DeploymentService slug.gsub(/[^-a-z0-9]/, '-').gsub(/^-+/, '') end - def build_kubeclient!(api_path: 'api', api_version: 'v1') + def build_kube_client!(api_groups: ['api'], api_version: 'v1') raise "Incomplete settings" unless api_url && actual_namespace && token - ::Kubeclient::Client.new( - join_api_url(api_path), + Gitlab::Kubernetes::KubeClient.new( + api_url, + api_groups, api_version, auth_options: kubeclient_auth_options, ssl_options: kubeclient_ssl_options, @@ -196,7 +197,7 @@ class KubernetesService < DeploymentService # Returns a hash of all pods in the namespace def read_pods - kubeclient = build_kubeclient! + kubeclient = build_kube_client! kubeclient.get_pods(namespace: actual_namespace).as_json rescue Kubeclient::HttpError => err @@ -220,15 +221,6 @@ class KubernetesService < DeploymentService { bearer_token: token } end - def join_api_url(api_path) - url = URI.parse(api_url) - prefix = url.path.sub(%r{/+\z}, '') - - url.path = [prefix, api_path].join("/") - - url.to_s - end - def terminal_auth { token: token, diff --git a/app/models/protected_ref_matcher.rb b/app/models/protected_ref_matcher.rb deleted file mode 100644 index bfa9180ac93..00000000000 --- a/app/models/protected_ref_matcher.rb +++ /dev/null @@ -1,56 +0,0 @@ -# frozen_string_literal: true - -class ProtectedRefMatcher - def initialize(protected_ref) - @protected_ref = protected_ref - end - - # Returns all protected refs that match the given ref name. - # This checks all records from the scope built up so far, and does - # _not_ return a relation. - # - # This method optionally takes in a list of `protected_refs` to search - # through, to avoid calling out to the database. - def self.matching(type, ref_name, protected_refs: nil) - (protected_refs || type.all).select { |protected_ref| protected_ref.matches?(ref_name) } - end - - # Returns all branches/tags (among the given list of refs [`Gitlab::Git::Branch`]) - # that match the current protected ref. - def matching(refs) - refs.select { |ref| @protected_ref.matches?(ref.name) } - end - - # Checks if the protected ref matches the given ref name. - def matches?(ref_name) - return false if @protected_ref.name.blank? - - exact_match?(ref_name) || wildcard_match?(ref_name) - end - - # Checks if this protected ref contains a wildcard - def wildcard? - @protected_ref.name && @protected_ref.name.include?('*') - end - - protected - - def exact_match?(ref_name) - @protected_ref.name == ref_name - end - - def wildcard_match?(ref_name) - return false unless wildcard? - - wildcard_regex === ref_name - end - - def wildcard_regex - @wildcard_regex ||= begin - name = @protected_ref.name.gsub('*', 'STAR_DONT_ESCAPE') - quoted_name = Regexp.quote(name) - regex_string = quoted_name.gsub('STAR_DONT_ESCAPE', '.*?') - /\A#{regex_string}\z/ - end - end -end diff --git a/app/models/ref_matcher.rb b/app/models/ref_matcher.rb new file mode 100644 index 00000000000..fa7d2c0f06c --- /dev/null +++ b/app/models/ref_matcher.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +class RefMatcher + def initialize(ref_name_or_pattern) + @ref_name_or_pattern = ref_name_or_pattern + end + + # Returns all branches/tags (among the given list of refs [`Gitlab::Git::Branch`]) + # that match the current protected ref. + def matching(refs) + refs.select { |ref| matches?(ref.name) } + end + + # Checks if the protected ref matches the given ref name. + def matches?(ref_name) + return false if @ref_name_or_pattern.blank? + + exact_match?(ref_name) || wildcard_match?(ref_name) + end + + # Checks if this protected ref contains a wildcard + def wildcard? + @ref_name_or_pattern && @ref_name_or_pattern.include?('*') + end + + protected + + def exact_match?(ref_name) + @ref_name_or_pattern == ref_name + end + + def wildcard_match?(ref_name) + return false unless wildcard? + + wildcard_regex === ref_name + end + + def wildcard_regex + @wildcard_regex ||= begin + name = @ref_name_or_pattern.gsub('*', 'STAR_DONT_ESCAPE') + quoted_name = Regexp.quote(name) + regex_string = quoted_name.gsub('STAR_DONT_ESCAPE', '.*?') + /\A#{regex_string}\z/ + end + end +end diff --git a/app/models/repository.rb b/app/models/repository.rb index cf255c8951f..929d28b9d88 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -580,7 +580,12 @@ class Repository end def rendered_readme - MarkupHelper.markup_unsafe(readme.name, readme.data, project: project, markdown_engine: :redcarpet) if readme + return unless readme + + context = { project: project } + context[:markdown_engine] = :redcarpet unless MarkupHelper.commonmark_for_repositories_enabled? + + MarkupHelper.markup_unsafe(readme.name, readme.data, context) end cache_method :rendered_readme diff --git a/app/models/user.rb b/app/models/user.rb index f21ca1c569f..0fcc952b5cd 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -257,6 +257,7 @@ class User < ActiveRecord::Base scope :order_recent_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'DESC')) } scope :order_oldest_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'ASC')) } scope :confirmed, -> { where.not(confirmed_at: nil) } + scope :by_username, -> (usernames) { iwhere(username: usernames) } # Limits the users to those that have TODOs, optionally in the given state. # @@ -444,11 +445,11 @@ class User < ActiveRecord::Base end def find_by_username(username) - iwhere(username: username).take + by_username(username).take end def find_by_username!(username) - iwhere(username: username).take! + by_username(username).take! end def find_by_personal_access_token(token_string) diff --git a/app/presenters/project_presenter.rb b/app/presenters/project_presenter.rb index 4c2f33213d6..6a54054badc 100644 --- a/app/presenters/project_presenter.rb +++ b/app/presenters/project_presenter.rb @@ -11,16 +11,18 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated presents :project + AnchorData = Struct.new(:enabled, :label, :link, :class_modifier) + MAX_TAGS_TO_SHOW = 3 + def statistics_anchors(show_auto_devops_callout:) [ + readme_anchor_data, + changelog_anchor_data, + contribution_guide_anchor_data, files_anchor_data, commits_anchor_data, branches_anchor_data, tags_anchor_data, - readme_anchor_data, - changelog_anchor_data, - license_anchor_data, - contribution_guide_anchor_data, gitlab_ci_anchor_data, autodevops_anchor_data(show_auto_devops_callout: show_auto_devops_callout), kubernetes_cluster_anchor_data @@ -31,7 +33,6 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated [ readme_anchor_data, changelog_anchor_data, - license_anchor_data, contribution_guide_anchor_data, autodevops_anchor_data(show_auto_devops_callout: show_auto_devops_callout), kubernetes_cluster_anchor_data, @@ -42,6 +43,10 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated def empty_repo_statistics_anchors [ + files_anchor_data, + commits_anchor_data, + branches_anchor_data, + tags_anchor_data, autodevops_anchor_data, kubernetes_cluster_anchor_data ].compact.select { |item| item.enabled } @@ -51,7 +56,6 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated [ new_file_anchor_data, readme_anchor_data, - license_anchor_data, autodevops_anchor_data, kubernetes_cluster_anchor_data ].compact.reject { |item| item.enabled } @@ -182,95 +186,101 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated end def files_anchor_data - OpenStruct.new(enabled: true, - label: _('Files (%{human_size})') % { human_size: storage_counter(statistics.total_repository_size) }, - link: project_tree_path(project)) + AnchorData.new(true, + _('Files (%{human_size})') % { human_size: storage_counter(statistics.total_repository_size) }, + empty_repo? ? nil : project_tree_path(project)) end def commits_anchor_data - OpenStruct.new(enabled: true, - label: n_('Commit (%{commit_count})', 'Commits (%{commit_count})', statistics.commit_count) % { commit_count: number_with_delimiter(statistics.commit_count) }, - link: project_commits_path(project, repository.root_ref)) + AnchorData.new(true, + n_('Commit (%{commit_count})', 'Commits (%{commit_count})', statistics.commit_count) % { commit_count: number_with_delimiter(statistics.commit_count) }, + empty_repo? ? nil : project_commits_path(project, repository.root_ref)) end def branches_anchor_data - OpenStruct.new(enabled: true, - label: n_('Branch (%{branch_count})', 'Branches (%{branch_count})', repository.branch_count) % { branch_count: number_with_delimiter(repository.branch_count) }, - link: project_branches_path(project)) + AnchorData.new(true, + n_('Branch (%{branch_count})', 'Branches (%{branch_count})', repository.branch_count) % { branch_count: number_with_delimiter(repository.branch_count) }, + empty_repo? ? nil : project_branches_path(project)) end def tags_anchor_data - OpenStruct.new(enabled: true, - label: n_('Tag (%{tag_count})', 'Tags (%{tag_count})', repository.tag_count) % { tag_count: number_with_delimiter(repository.tag_count) }, - link: project_tags_path(project)) + AnchorData.new(true, + n_('Tag (%{tag_count})', 'Tags (%{tag_count})', repository.tag_count) % { tag_count: number_with_delimiter(repository.tag_count) }, + empty_repo? ? nil : project_tags_path(project)) end def new_file_anchor_data if current_user && can_current_user_push_to_default_branch? - OpenStruct.new(enabled: false, - label: _('New file'), - link: project_new_blob_path(project, default_branch || 'master'), - class_modifier: 'new') + AnchorData.new(false, + _('New file'), + project_new_blob_path(project, default_branch || 'master'), + 'new') end end def readme_anchor_data if current_user && can_current_user_push_to_default_branch? && repository.readme.nil? - OpenStruct.new(enabled: false, - label: _('Add Readme'), - link: add_readme_path) + AnchorData.new(false, + _('Add Readme'), + add_readme_path) elsif repository.readme - OpenStruct.new(enabled: true, - label: _('Readme'), - link: default_view != 'readme' ? readme_path : '#readme') + AnchorData.new(true, + _('Readme'), + default_view != 'readme' ? readme_path : '#readme') end end def changelog_anchor_data if current_user && can_current_user_push_to_default_branch? && repository.changelog.blank? - OpenStruct.new(enabled: false, - label: _('Add Changelog'), - link: add_changelog_path) + AnchorData.new(false, + _('Add Changelog'), + add_changelog_path) elsif repository.changelog.present? - OpenStruct.new(enabled: true, - label: _('Changelog'), - link: changelog_path) + AnchorData.new(true, + _('Changelog'), + changelog_path) end end def license_anchor_data - if current_user && can_current_user_push_to_default_branch? && repository.license_blob.blank? - OpenStruct.new(enabled: false, - label: _('Add License'), - link: add_license_path) - elsif repository.license_blob.present? - OpenStruct.new(enabled: true, - label: license_short_name, - link: license_path) + if repository.license_blob.present? + AnchorData.new(true, + license_short_name, + license_path) + else + if current_user && can_current_user_push_to_default_branch? + AnchorData.new(false, + _('Add license'), + add_license_path) + else + AnchorData.new(false, + _('No license. All rights reserved'), + nil) + end end end def contribution_guide_anchor_data if current_user && can_current_user_push_to_default_branch? && repository.contribution_guide.blank? - OpenStruct.new(enabled: false, - label: _('Add Contribution guide'), - link: add_contribution_guide_path) + AnchorData.new(false, + _('Add Contribution guide'), + add_contribution_guide_path) elsif repository.contribution_guide.present? - OpenStruct.new(enabled: true, - label: _('Contribution guide'), - link: contribution_guide_path) + AnchorData.new(true, + _('Contribution guide'), + contribution_guide_path) end end def autodevops_anchor_data(show_auto_devops_callout: false) if current_user && can?(current_user, :admin_pipeline, project) && repository.gitlab_ci_yml.blank? && !show_auto_devops_callout - OpenStruct.new(enabled: auto_devops_enabled?, - label: auto_devops_enabled? ? _('Auto DevOps enabled') : _('Enable Auto DevOps'), - link: project_settings_ci_cd_path(project, anchor: 'autodevops-settings')) + AnchorData.new(auto_devops_enabled?, + auto_devops_enabled? ? _('Auto DevOps enabled') : _('Enable Auto DevOps'), + project_settings_ci_cd_path(project, anchor: 'autodevops-settings')) elsif auto_devops_enabled? - OpenStruct.new(enabled: true, - label: _('Auto DevOps enabled'), - link: nil) + AnchorData.new(true, + _('Auto DevOps enabled'), + nil) end end @@ -282,32 +292,48 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated cluster_link = new_project_cluster_path(project) end - OpenStruct.new(enabled: !clusters.empty?, - label: clusters.empty? ? _('Add Kubernetes cluster') : _('Kubernetes configured'), - link: cluster_link) + AnchorData.new(!clusters.empty?, + clusters.empty? ? _('Add Kubernetes cluster') : _('Kubernetes configured'), + cluster_link) end end def gitlab_ci_anchor_data if current_user && can_current_user_push_code? && repository.gitlab_ci_yml.blank? && !auto_devops_enabled? - OpenStruct.new(enabled: false, - label: _('Set up CI/CD'), - link: add_ci_yml_path) + AnchorData.new(false, + _('Set up CI/CD'), + add_ci_yml_path) elsif repository.gitlab_ci_yml.present? - OpenStruct.new(enabled: true, - label: _('CI/CD configuration'), - link: ci_configuration_path) + AnchorData.new(true, + _('CI/CD configuration'), + ci_configuration_path) end end def koding_anchor_data if current_user && can_current_user_push_code? && koding_enabled? && repository.koding_yml.blank? - OpenStruct.new(enabled: false, - label: _('Set up Koding'), - link: add_koding_stack_path) + AnchorData.new(false, + _('Set up Koding'), + add_koding_stack_path) end end + def tags_to_show + project.tag_list.take(MAX_TAGS_TO_SHOW) + end + + def count_of_extra_tags_not_shown + if project.tag_list.count > MAX_TAGS_TO_SHOW + project.tag_list.count - MAX_TAGS_TO_SHOW + else + 0 + end + end + + def has_extra_tags? + count_of_extra_tags_not_shown > 0 + end + private def filename_path(filename) diff --git a/app/serializers/build_details_entity.rb b/app/serializers/build_details_entity.rb index 271ff668eda..b107fc26f18 100644 --- a/app/serializers/build_details_entity.rb +++ b/app/serializers/build_details_entity.rb @@ -36,6 +36,10 @@ class BuildDetailsEntity < JobEntity erase_project_job_path(project, build) end + expose :terminal_path, if: -> (*) { can_create_build_terminal? } do |build| + terminal_project_job_path(project, build) + end + expose :merge_request, if: -> (*) { can?(current_user, :read_merge_request, build.merge_request) } do expose :iid do |build| build.merge_request.iid @@ -69,4 +73,8 @@ class BuildDetailsEntity < JobEntity def project build.project end + + def can_create_build_terminal? + can?(current_user, :create_build_terminal, build) && build.has_terminal? + end end diff --git a/app/services/preview_markdown_service.rb b/app/services/preview_markdown_service.rb index 11b996ed4b6..de8757006f1 100644 --- a/app/services/preview_markdown_service.rb +++ b/app/services/preview_markdown_service.rb @@ -43,6 +43,10 @@ class PreviewMarkdownService < BaseService end def markdown_engine - CacheMarkdownField::MarkdownEngine.from_version(params[:markdown_version].to_i) + if params[:legacy_render] + :redcarpet + else + CacheMarkdownField::MarkdownEngine.from_version(params[:markdown_version].to_i) + end end end diff --git a/app/validators/branch_filter_validator.rb b/app/validators/branch_filter_validator.rb new file mode 100644 index 00000000000..ef482aaaa63 --- /dev/null +++ b/app/validators/branch_filter_validator.rb @@ -0,0 +1,35 @@ +# BranchFilterValidator +# +# Custom validator for branch names. Squishes whitespace and ignores empty +# string. This only checks that a string is a valid git branch name. It does +# not check whether a branch already exists. +# +# Example: +# +# class Webhook < ActiveRecord::Base +# validates :push_events_branch_filter, branch_name: true +# end +# +class BranchFilterValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + value.squish! unless value.nil? + + if value.present? + value_without_wildcards = value.tr('*', 'x') + + unless Gitlab::GitRefValidator.validate(value_without_wildcards) + record.errors[attribute] << "is not a valid branch name" + end + + unless value.length <= 4000 + record.errors[attribute] << "is longer than the allowed length of 4000 characters." + end + end + end + + private + + def contains_wildcard?(value) + value.include?('*') + end +end diff --git a/app/views/admin/application_settings/_usage.html.haml b/app/views/admin/application_settings/_usage.html.haml index 2495defb6a7..788595877ea 100644 --- a/app/views/admin/application_settings/_usage.html.haml +++ b/app/views/admin/application_settings/_usage.html.haml @@ -2,7 +2,7 @@ = form_errors(@application_setting) %fieldset - .form-group + .form-group.mb-2 .form-check = f.check_box :version_check_enabled, class: 'form-check-input' = f.label :version_check_enabled, class: 'form-check-label' do @@ -16,23 +16,26 @@ .form-check = f.check_box :usage_ping_enabled, disabled: !can_be_configured, class: 'form-check-input' = f.label :usage_ping_enabled, class: 'form-check-label' do - Enable usage ping + = _('Enable usage ping') .form-text.text-muted - if can_be_configured - To help improve GitLab and its user experience, GitLab will - periodically collect usage information. - = link_to 'Learn more', help_page_path("user/admin_area/settings/usage_statistics", anchor: "usage-ping") - about what information is shared with GitLab Inc. Visit - = link_to _('Cohorts'), instance_statistics_cohorts_path(anchor: 'usage-ping') - to see the JSON payload sent. + %p.mb-2= _('To help improve GitLab and its user experience, GitLab will periodically collect usage information.') + + - usage_ping_path = help_page_path('user/admin_area/settings/usage_statistics', anchor: 'usage-ping') + - usage_ping_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: usage_ping_path } + %p.mb-2= s_('%{usage_ping_link_start}Learn more%{usage_ping_link_end} about what information is shared with GitLab Inc.').html_safe % { usage_ping_link_start: usage_ping_link_start, usage_ping_link_end: '</a>'.html_safe } + + %button.btn.js-usage-ping-payload-trigger{ type: 'button' } + .js-spinner.d-none= icon('spinner spin') + .js-text.d-inline= _('Preview payload') + %pre.usage-data.js-usage-ping-payload.js-syntax-highlight.code.highlight.mt-2.d-none{ data: { endpoint: usage_data_admin_application_settings_path(format: :html) } } - else - The usage ping is disabled, and cannot be configured through this - form. For more information, see the documentation on - = succeed '.' do - = link_to 'deactivating the usage ping', help_page_path('user/admin_area/settings/usage_statistics', anchor: 'deactivate-the-usage-ping') - .form-group + = _('The usage ping is disabled, and cannot be configured through this form.') + - deactivating_usage_ping_path = help_page_path('user/admin_area/settings/usage_statistics', anchor: 'deactivate-the-usage-ping') + - deactivating_usage_ping_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: deactivating_usage_ping_path } + = s_('For more information, see the documentation on %{deactivating_usage_ping_link_start}deactivating the usage ping%{deactivating_usage_ping_link_end}.').html_safe % { deactivating_usage_ping_link_start: deactivating_usage_ping_link_start, deactivating_usage_ping_link_end: '</a>'.html_safe } + .form-group.mt-3 = f.label :instance_statistics_visibility_private, _('Instance Statistics visibility') = f.select :instance_statistics_visibility_private, options_for_select({_('All users') => false, _('Only admins') => true}, Gitlab::CurrentSettings.instance_statistics_visibility_private?), {}, class: 'form-control' = f.submit 'Save changes', class: "btn btn-success" - diff --git a/app/views/devise/sessions/_new_base.html.haml b/app/views/devise/sessions/_new_base.html.haml index 0ee563ac066..17a9c8df872 100644 --- a/app/views/devise/sessions/_new_base.html.haml +++ b/app/views/devise/sessions/_new_base.html.haml @@ -1,9 +1,9 @@ = form_for(resource, as: resource_name, url: session_path(resource_name), html: { class: 'new_user gl-show-field-errors', 'aria-live' => 'assertive'}) do |f| .form-group - = f.label "Username or email", for: "user_login" + = f.label "Username or email", for: "user_login", class: 'label-bold' = f.text_field :login, class: "form-control top", autofocus: "autofocus", autocapitalize: "off", autocorrect: "off", required: true, title: "This field is required." .form-group - = f.label :password + = f.label :password, class: 'label-bold' = f.password_field :password, class: "form-control bottom", required: true, title: "This field is required." - if devise_mapping.rememberable? .remember-me diff --git a/app/views/devise/shared/_omniauth_box.html.haml b/app/views/devise/shared/_omniauth_box.html.haml index 3723814debe..269a3721e06 100644 --- a/app/views/devise/shared/_omniauth_box.html.haml +++ b/app/views/devise/shared/_omniauth_box.html.haml @@ -1,14 +1,17 @@ -.omniauth-container - %p - %span.light - Sign in with - - providers = enabled_button_based_providers +.omniauth-container.prepend-top-15 + %label.label-bold.d-block + Sign in with + - providers = enabled_button_based_providers + .d-flex.justify-content-between.flex-wrap - providers.each do |provider| - %span.light - - has_icon = provider_has_icon?(provider) - = link_to provider_image_tag(provider), omniauth_authorize_path(:user, provider), method: :post, class: 'oauth-login' + (has_icon ? ' oauth-image-link' : ' btn'), id: "oauth-login-#{provider}" - %fieldset.prepend-top-10.remember-me - %label - = check_box_tag :remember_me, nil, false, class: 'remember-me-checkbox' + - has_icon = provider_has_icon?(provider) + = link_to omniauth_authorize_path(:user, provider), method: :post, class: 'btn d-flex align-items-center omniauth-btn text-left oauth-login', id: "oauth-login-#{provider}" do + - if has_icon + = provider_image_tag(provider) %span - Remember me + = label_for_provider(provider) + %fieldset.remember-me + %label + = check_box_tag :remember_me, nil, false, class: 'remember-me-checkbox' + %span + Remember me diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml index ee7369f54a9..90ed20404c5 100644 --- a/app/views/devise/shared/_signup_box.html.haml +++ b/app/views/devise/shared/_signup_box.html.haml @@ -4,24 +4,24 @@ .devise-errors = devise_error_messages! .form-group - = f.label :name, 'Full name' + = f.label :name, 'Full name', class: 'label-bold' = f.text_field :name, class: "form-control top", required: true, title: "This field is required." .username.form-group - = f.label :username + = f.label :username, class: 'label-bold' = f.text_field :username, class: "form-control middle", pattern: Gitlab::PathRegex::NAMESPACE_FORMAT_REGEX_JS, required: true, title: 'Please create a username with only alphanumeric characters.' %p.validation-error.hide Username is already taken. %p.validation-success.hide Username is available. %p.validation-pending.hide Checking username availability... .form-group - = f.label :email + = f.label :email, class: 'label-bold' = f.email_field :email, class: "form-control middle", required: true, title: "Please provide a valid email address." .form-group - = f.label :email_confirmation + = f.label :email_confirmation, class: 'label-bold' = f.email_field :email_confirmation, class: "form-control middle", required: true, title: "Please retype the email address." .form-group.append-bottom-20#password-strength - = f.label :password + = f.label :password, class: 'label-bold' = f.password_field :password, class: "form-control bottom", required: true, pattern: ".{#{@minimum_password_length},}", title: "Minimum length is #{@minimum_password_length} characters." - %p.gl-field-hint Minimum length is #{@minimum_password_length} characters + %p.gl-field-hint.text-secondary Minimum length is #{@minimum_password_length} characters - if Gitlab::CurrentSettings.current_application_settings.enforce_terms? .form-group = check_box_tag :terms_opt_in, '1', false, required: true @@ -34,8 +34,3 @@ = recaptcha_tags .submit-container = f.submit "Register", class: "btn-register btn" -.clearfix.submit-container - %p - %span.light Didn't receive a confirmation email? - = succeed '.' do - = link_to "Request a new one", new_confirmation_path(:user) diff --git a/app/views/groups/labels/index.html.haml b/app/views/groups/labels/index.html.haml index d63ef477177..e6821009d03 100644 --- a/app/views/groups/labels/index.html.haml +++ b/app/views/groups/labels/index.html.haml @@ -1,7 +1,6 @@ - @no_container = true - page_title "Labels" - can_admin_label = can?(current_user, :admin_label, @group) -- hide = @available_labels.empty? || (params[:page].present? && params[:page] != '1') - issuables = ['issues', 'merge requests'] - search = params[:search] @@ -27,8 +26,7 @@ .labels-container.prepend-top-5 - if @labels.any? .other-labels - - if can_admin_label - %h5{ class: ('hide' if hide) } Labels + %h5= _('Labels') %ul.content-list.manage-labels-list.js-other-labels = render partial: 'shared/label', subject: @group, collection: @labels, as: :label, locals: { use_label_priority: false } = paginate @labels, theme: 'gitlab' diff --git a/app/views/instance_statistics/cohorts/index.html.haml b/app/views/instance_statistics/cohorts/index.html.haml index 5e9a8c083af..e135bab10d8 100644 --- a/app/views/instance_statistics/cohorts/index.html.haml +++ b/app/views/instance_statistics/cohorts/index.html.haml @@ -1,16 +1,16 @@ -- breadcrumb_title "Cohorts" +- breadcrumb_title _("Cohorts") - @no_container = true %div{ class: container_class } - if @cohorts = render 'cohorts_table' - = render 'usage_ping' - else .bs-callout.bs-callout-warning.clearfix %p - User cohorts are only shown when the - = link_to 'usage ping', help_page_path('user/admin_area/settings/usage_statistics', anchor: 'usage-ping'), target: '_blank' - is enabled. To enable it and see user cohorts, - visit - = succeed '.' do - = link_to 'application settings', admin_application_settings_path(anchor: 'usage-statistics') + - usage_ping_path = help_page_path('user/admin_area/settings/usage_statistics', anchor: 'usage-ping') + - usage_ping_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: usage_ping_path } + = s_('User Cohorts are only shown when the %{usage_ping_link_start}usage ping%{usage_ping_link_end} is enabled.').html_safe % { usage_ping_link_start: usage_ping_link_start, usage_ping_link_end: '</a>'.html_safe } + - if current_user.admin? + - application_settings_path = admin_application_settings_path(anchor: 'usage-statistics') + - application_settings_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: application_settings_path } + = s_('To enable it and see User Cohorts, visit %{application_settings_link_start}application settings%{application_settings_link_end}.').html_safe % { application_settings_link_start: application_settings_link_start, application_settings_link_end: '</a>'.html_safe } diff --git a/app/views/instance_statistics/conversational_development_index/_disabled.html.haml b/app/views/instance_statistics/conversational_development_index/_disabled.html.haml index 0a741b50960..0a5717f75e1 100644 --- a/app/views/instance_statistics/conversational_development_index/_disabled.html.haml +++ b/app/views/instance_statistics/conversational_development_index/_disabled.html.haml @@ -1,9 +1,14 @@ .container.convdev-empty .col-sm-12.justify-content-center.text-center = custom_icon('convdev_no_index') - %h4 Usage ping is not enabled - %p - ConvDev is only shown when the - = link_to 'usage ping', help_page_path('user/admin_area/settings/usage_statistics'), target: '_blank' - is enabled. Enable usage ping to get an overview of how you are using GitLab from a feature perspective - = link_to 'Enable usage ping', admin_application_settings_path(anchor: 'usage-statistics'), class: 'btn btn-primary' + %h4= _('Usage ping is not enabled') + - if !current_user.admin? + %p + - usage_ping_path = help_page_path('user/admin_area/settings/usage_statistics', anchor: 'usage-ping') + - usage_ping_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: usage_ping_path } + = s_('In order to enable instance-level analytics, please ask an admin to enable %{usage_ping_link_start}usage ping%{usage_ping_link_end}.').html_safe % { usage_ping_link_start: usage_ping_link_start, usage_ping_link_end: '</a>'.html_safe } + - if current_user.admin? + %p + = _('Enable usage ping to get an overview of how you are using GitLab from a feature perspective.') + - if current_user.admin? + = link_to _('Enable usage ping'), admin_application_settings_path(anchor: 'usage-statistics'), class: 'btn btn-primary' diff --git a/app/views/instance_statistics/conversational_development_index/index.html.haml b/app/views/instance_statistics/conversational_development_index/index.html.haml index dd63b98376f..1e7db4982d6 100644 --- a/app/views/instance_statistics/conversational_development_index/index.html.haml +++ b/app/views/instance_statistics/conversational_development_index/index.html.haml @@ -1,12 +1,13 @@ - @no_container = true -- page_title 'ConvDev Index' +- page_title _('ConvDev Index') +- usage_ping_enabled = Gitlab::CurrentSettings.usage_ping_enabled .container - - if show_callout?('convdev_intro_callout_dismissed') + - if usage_ping_enabled && show_callout?('convdev_intro_callout_dismissed') = render 'callout' .prepend-top-default - - if !Gitlab::CurrentSettings.usage_ping_enabled + - if !usage_ping_enabled = render 'disabled' - elsif @metric.blank? = render 'no_data' diff --git a/app/views/layouts/nav/sidebar/_instance_statistics.html.haml b/app/views/layouts/nav/sidebar/_instance_statistics.html.haml index b8ff448f261..57180f27146 100644 --- a/app/views/layouts/nav/sidebar/_instance_statistics.html.haml +++ b/app/views/layouts/nav/sidebar/_instance_statistics.html.haml @@ -18,16 +18,17 @@ %strong.fly-out-top-item-name = _('ConvDev Index') - = nav_link(controller: :cohorts) do - = link_to instance_statistics_cohorts_path do - .nav-icon-container - = sprite_icon('users') - %span.nav-item-name - = _('Cohorts') - %ul.sidebar-sub-level-items.is-fly-out-only - = nav_link(controller: :cohorts, html_options: { class: "fly-out-top-item" } ) do - = link_to instance_statistics_cohorts_path do - %strong.fly-out-top-item-name - = _('Cohorts') + - if Gitlab::CurrentSettings.usage_ping_enabled + = nav_link(controller: :cohorts) do + = link_to instance_statistics_cohorts_path do + .nav-icon-container + = sprite_icon('users') + %span.nav-item-name + = _('Cohorts') + %ul.sidebar-sub-level-items.is-fly-out-only + = nav_link(controller: :cohorts, html_options: { class: "fly-out-top-item" } ) do + = link_to instance_statistics_cohorts_path do + %strong.fly-out-top-item-name + = _('Cohorts') = render 'shared/sidebar_toggle_button' diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml index 6f08a294c5d..9f79feb4ddd 100644 --- a/app/views/profiles/show.html.haml +++ b/app/views/profiles/show.html.haml @@ -77,7 +77,7 @@ = f.text_field :name, required: true, readonly: true, wrapper: { class: 'col-md-9' }, help: "Your name was automatically set based on your #{ attribute_provider_label(:name) } account, so people you know can recognize you." - else - = f.text_field :name, required: true, wrapper: { class: 'col-md-9' }, help: "Enter your name, so people you know can recognize you." + = f.text_field :name, label: 'Full name', required: true, wrapper: { class: 'col-md-9' }, help: "Enter your name, so people you know can recognize you." = f.text_field :id, readonly: true, label: 'User ID', wrapper: { class: 'col-md-3' } - if @user.read_only_attribute?(:email) diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index 1b6c4193c4d..ced6a2a0399 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -1,16 +1,35 @@ - empty_repo = @project.empty_repo? -.project-home-panel.text-center{ class: ("empty-project" if empty_repo) } +- license = @project.license_anchor_data +.project-home-panel{ class: ("empty-project" if empty_repo) } .limit-container-width{ class: container_class } - .avatar-container.s70.project-avatar - = project_icon(@project, alt: @project.name, class: 'avatar s70 avatar-tile', width: 70, height: 70) - %h1.project-title.qa-project-name - = @project.name - %span.visibility-icon.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@project) } - = visibility_level_icon(@project.visibility_level, fw: false) + .project-header.d-flex.flex-row.flex-wrap.align-items-center.append-bottom-8 + .project-title-row.d-flex.align-items-center + .avatar-container.project-avatar.float-none + = project_icon(@project, alt: @project.name, class: 'avatar avatar-tile') + %h1.project-title.d-flex.align-items-baseline.qa-project-name + = @project.name + .project-metadata.d-flex.flex-row.flex-wrap.align-items-baseline + .project-visibility.d-inline-flex.align-items-baseline.visibility-icon.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@project) } + = visibility_level_icon(@project.visibility_level, fw: false, options: {class: 'icon'}) + = visibility_level_label(@project.visibility_level) + - if license.present? + .project-license.d-inline-flex.align-items-baseline + = link_to_if license.link, sprite_icon('scale', size: 16, css_class: 'icon') + license.label, license.link, class: license.enabled ? 'btn btn-link btn-secondary-hover-link' : 'btn btn-link' + - if @project.tag_list.present? + .project-tag-list.d-inline-flex.align-items-baseline.has-tooltip{ data: { container: 'body' }, title: @project.has_extra_tags? ? @project.tag_list.join(', ') : nil } + = sprite_icon('tag', size: 16, css_class: 'icon') + = @project.tags_to_show + - if @project.has_extra_tags? + = _("+ %{count} more") % { count: @project.count_of_extra_tags_not_shown } .project-home-desc - if @project.description.present? - = markdown_field(@project, :description) + .project-description + .project-description-markdown.read-more-container + = markdown_field(@project, :description) + %button.btn.btn-blank.btn-link.text-secondary.js-read-more-trigger.text-secondary.d-lg-none{ type: "button" } + = _("Read more") + - if can?(current_user, :read_project, @project) .text-secondary.prepend-top-8 = s_('ProjectPage|Project ID: %{project_id}') % { project_id: @project.id } @@ -25,34 +44,42 @@ - deleted_message = s_('ForkedFromProjectPath|Forked from %{project_name} (deleted)') = deleted_message % { project_name: fork_source_name(@project) } - .project-badges.prepend-top-default.append-bottom-default - - @project.badges.each do |badge| - %a.append-right-8{ href: badge.rendered_link_url(@project), - target: '_blank', - rel: 'noopener noreferrer' }> - %img.project-badge{ src: badge.rendered_image_url(@project), - 'aria-hidden': true, - alt: '' }> - - .project-repo-buttons - .count-buttons + - if @project.badges.present? + .project-badges.prepend-top-default.append-bottom-default + - @project.badges.each do |badge| + %a.append-right-8{ href: badge.rendered_link_url(@project), + target: '_blank', + rel: 'noopener noreferrer' }> + %img.project-badge{ src: badge.rendered_image_url(@project), + 'aria-hidden': true, + alt: 'Project badge' }> + + .project-repo-buttons.d-inline-flex.flex-wrap + .count-buttons.d-inline-flex = render 'projects/buttons/star' = render 'projects/buttons/fork' - %span.d-none.d-sm-inline - - if can?(current_user, :download_code, @project) - .project-clone-holder - = render "shared/clone_panel" + - if can?(current_user, :download_code, @project) + .project-clone-holder.d-inline-flex.d-sm-none + = render "shared/mobile_clone_panel" - - if show_xcode_link?(@project) - .project-action-button.project-xcode.inline - = render "projects/buttons/xcode_link" + .project-clone-holder.d-none.d-sm-inline-flex + = render "shared/clone_panel" - - if current_user - - if can?(current_user, :download_code, @project) + - if show_xcode_link?(@project) + .project-action-button.project-xcode.inline + = render "projects/buttons/xcode_link" + + - if current_user + - if can?(current_user, :download_code, @project) + .d-none.d-sm-inline-flex = render 'projects/buttons/download', project: @project, ref: @ref + .d-none.d-sm-inline-flex = render 'projects/buttons/dropdown' + .d-none.d-sm-inline-flex = render 'projects/buttons/koding' + .d-none.d-sm-inline-flex = render 'shared/notifications/button', notification_setting: @notification_setting + .d-none.d-sm-inline-flex = render 'shared/members/access_request_buttons', source: @project diff --git a/app/views/projects/_stat_anchor_list.html.haml b/app/views/projects/_stat_anchor_list.html.haml index 15ec58289e3..4cf49f3cf62 100644 --- a/app/views/projects/_stat_anchor_list.html.haml +++ b/app/views/projects/_stat_anchor_list.html.haml @@ -1,7 +1,7 @@ - anchors = local_assigns.fetch(:anchors, []) - return unless anchors.any? -%ul.nav.justify-content-center +%ul.nav - anchors.each do |anchor| %li.nav-item = link_to_if anchor.link, anchor.label, anchor.link, class: anchor.enabled ? 'nav-link stat-link' : "nav-link btn btn-#{anchor.class_modifier || 'missing'}" do diff --git a/app/views/projects/_wiki.html.haml b/app/views/projects/_wiki.html.haml index 5646dc464f8..5adca007f7e 100644 --- a/app/views/projects/_wiki.html.haml +++ b/app/views/projects/_wiki.html.haml @@ -2,7 +2,7 @@ %div{ class: container_class } .prepend-top-default.append-bottom-default .wiki - = render_wiki_content(@wiki_home) + = render_wiki_content(@wiki_home, legacy_render_context(params)) - else - can_create_wiki = can?(current_user, :create_wiki, @project) .project-home-empty{ class: [('row-content-block' if can_create_wiki), ('content-block' unless can_create_wiki)] } diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml index a4b1b496b69..cf273aab108 100644 --- a/app/views/projects/blob/_blob.html.haml +++ b/app/views/projects/blob/_blob.html.haml @@ -5,6 +5,7 @@ %ul.blob-commit-info = render 'projects/commits/commit', commit: @last_commit, project: @project, ref: @ref + = render_if_exists 'projects/blob/owners', blob: blob = render "projects/blob/auxiliary_viewer", blob: blob #blob-content-holder.blob-content-holder diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml index 27cf040da7c..fdab8a53b41 100644 --- a/app/views/projects/blob/edit.html.haml +++ b/app/views/projects/blob/edit.html.haml @@ -21,7 +21,7 @@ Write %li - = link_to '#preview', 'data-preview-url' => project_preview_blob_path(@project, @id) do + = link_to '#preview', 'data-preview-url' => project_preview_blob_path(@project, @id, legacy_render: params[:legacy_render]) do = editing_preview_title(@blob.name) = form_tag(project_update_blob_path(@project, @id), method: :put, class: 'js-quick-submit js-requires-input js-edit-blob-form', data: blob_editor_paths) do diff --git a/app/views/projects/blob/preview.html.haml b/app/views/projects/blob/preview.html.haml index da2cef17e8a..eb65cd90ea8 100644 --- a/app/views/projects/blob/preview.html.haml +++ b/app/views/projects/blob/preview.html.haml @@ -2,7 +2,7 @@ .diff-content - if markup?(@blob.name) .file-content.wiki - = markup(@blob.name, @content) + = markup(@blob.name, @content, legacy_render_context(params)) - else .file-content.code.js-syntax-highlight - unless @diff_lines.empty? diff --git a/app/views/projects/blob/viewers/_markup.html.haml b/app/views/projects/blob/viewers/_markup.html.haml index 230305b488d..bd12cadf240 100644 --- a/app/views/projects/blob/viewers/_markup.html.haml +++ b/app/views/projects/blob/viewers/_markup.html.haml @@ -1,4 +1,6 @@ - blob = viewer.blob -- rendered_markup = blob.rendered_markup if blob.respond_to?(:rendered_markup) +- context = legacy_render_context(params) +- unless context[:markdown_engine] == :redcarpet + - context[:rendered] = blob.rendered_markup if blob.respond_to?(:rendered_markup) .file-content.wiki - = markup(blob.name, blob.data, rendered: rendered_markup) + = markup(blob.name, blob.data, context) diff --git a/app/views/projects/buttons/_fork.html.haml b/app/views/projects/buttons/_fork.html.haml index f880556a9f7..8da27ca7cb3 100644 --- a/app/views/projects/buttons/_fork.html.haml +++ b/app/views/projects/buttons/_fork.html.haml @@ -1,17 +1,17 @@ - unless @project.empty_repo? - if current_user && can?(current_user, :fork_project, @project) - - if current_user.already_forked?(@project) && current_user.manageable_namespaces.size < 2 - = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: _('Go to your fork'), class: 'btn has-tooltip' do - = custom_icon('icon_fork') - %span= s_('GoToYourFork|Fork') - - else - - can_create_fork = current_user.can?(:create_fork) - = link_to new_project_fork_path(@project), - class: "btn btn-default #{'has-tooltip disabled' unless can_create_fork}", - title: (_('You have reached your project limit') unless can_create_fork) do - = custom_icon('icon_fork') - %span= s_('CreateNewFork|Fork') - .count-with-arrow - %span.arrow - = link_to project_forks_path(@project), title: n_('Fork', 'Forks', @project.forks_count), class: 'count' do - = @project.forks_count + .count-badge.d-inline-flex.align-item-stretch.append-right-8 + %span.fork-count.count-badge-count.d-flex.align-items-center + = link_to project_forks_path(@project), title: n_(s_('ProjectOverview|Fork'), s_('ProjectOverview|Forks'), @project.forks_count), class: 'count' do + = @project.forks_count + - if current_user.already_forked?(@project) && current_user.manageable_namespaces.size < 2 + = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: s_('ProjectOverview|Go to your fork'), class: 'btn btn-default has-tooltip count-badge-button d-flex align-items-center fork-btn' do + = sprite_icon('fork', { css_class: 'icon' }) + %span= s_('ProjectOverview|Fork') + - else + - can_create_fork = current_user.can?(:create_fork) + = link_to new_project_fork_path(@project), + class: "btn btn-default has-tooltip count-badge-button d-flex align-items-center fork-btn #{'has-tooltip disabled' unless can_create_fork}", + title: (s_('ProjectOverview|You have reached your project limit') unless can_create_fork) do + = sprite_icon('fork', { css_class: 'icon' }) + %span= s_('ProjectOverview|Fork') diff --git a/app/views/projects/buttons/_star.html.haml b/app/views/projects/buttons/_star.html.haml index a2dc2730ecc..0d04ecb3a58 100644 --- a/app/views/projects/buttons/_star.html.haml +++ b/app/views/projects/buttons/_star.html.haml @@ -1,21 +1,19 @@ - if current_user - %button.btn.btn-default.star-btn.toggle-star{ type: "button", data: { endpoint: toggle_star_project_path(@project, :json) } }> - - if current_user.starred?(@project) - = sprite_icon('star') - %span.starred= _('Unstar') - - else - = sprite_icon('star-o') - %span= s_('StarProject|Star') - .count-with-arrow - %span.arrow - %span.count.star-count + .count-badge.d-inline-flex.align-item-stretch.append-right-8 + %span.star-count.count-badge-count.d-flex.align-items-center = @project.star_count + %button.count-badge-button.btn.btn-default.d-flex.align-items-center.star-btn.toggle-star{ type: "button", data: { endpoint: toggle_star_project_path(@project, :json) } } + - if current_user.starred?(@project) + = sprite_icon('star', { css_class: 'icon' }) + %span.starred= s_('ProjectOverview|Unstar') + - else + = sprite_icon('star-o', { css_class: 'icon' }) + %span= s_('ProjectOverview|Star') - else - = link_to new_user_session_path, class: 'btn has-tooltip star-btn', title: _('You must sign in to star a project') do - = sprite_icon('star') - #{ s_('StarProject|Star') } - .count-with-arrow - %span.arrow - %span.count + .count-badge.d-inline-flex.align-item-stretch.append-right-8 + %span.star-count.count-badge-count.d-flex.align-items-center = @project.star_count + = link_to new_user_session_path, class: 'btn btn-default has-tooltip count-badge-button d-flex align-items-center star-btn', title: s_('ProjectOverview|You must sign in to star a project') do + = sprite_icon('star-o', { css_class: 'icon' }) + %span= s_('ProjectOverview|Star') diff --git a/app/views/projects/clusters/user/_form.html.haml b/app/views/projects/clusters/user/_form.html.haml index e8ef0008802..1f81e024ab9 100644 --- a/app/views/projects/clusters/user/_form.html.haml +++ b/app/views/projects/clusters/user/_form.html.haml @@ -25,5 +25,14 @@ = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)'), class: 'label-bold' = platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace') + - if rbac_clusters_feature_enabled? + .form-group + .form-check + = platform_kubernetes_field.check_box :authorization_type, { class: 'form-check-input' }, 'rbac', 'abac' + = platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster (experimental)'), class: 'form-check-label label-bold' + .form-text.text-muted + = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).') + = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.') + .form-group = field.submit s_('ClusterIntegration|Add Kubernetes cluster'), class: 'btn btn-success' diff --git a/app/views/projects/clusters/user/_show.html.haml b/app/views/projects/clusters/user/_show.html.haml index 20a07d6695e..56b597d295a 100644 --- a/app/views/projects/clusters/user/_show.html.haml +++ b/app/views/projects/clusters/user/_show.html.haml @@ -26,5 +26,14 @@ = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)'), class: 'label-bold' = platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace') + - if rbac_clusters_feature_enabled? + .form-group + .form-check + = platform_kubernetes_field.check_box :authorization_type, { class: 'form-check-input', disabled: true }, 'rbac', 'abac' + = platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster (experimental)'), class: 'form-check-label label-bold' + .form-text.text-muted + = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).') + = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.') + .form-group = field.submit s_('ClusterIntegration|Save changes'), class: 'btn btn-success' diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml index d47dc3d8143..d104608b2fe 100644 --- a/app/views/projects/empty.html.haml +++ b/app/views/projects/empty.html.haml @@ -32,9 +32,13 @@ = _('Otherwise it is recommended you start with one of the options below.') .prepend-top-20 -%nav.project-stats{ class: container_class } - = render 'stat_anchor_list', anchors: @project.empty_repo_statistics_anchors - = render 'stat_anchor_list', anchors: @project.empty_repo_statistics_buttons +%nav.project-stats{ class: [container_class, ("limit-container-width" unless fluid_layout)] } + .scrolling-tabs-container.inner-page-scroll-tabs.is-smaller + .fade-left= icon('angle-left') + .fade-right= icon('angle-right') + .nav-links.scrolling-tabs + = render 'stat_anchor_list', anchors: @project.empty_repo_statistics_anchors + = render 'stat_anchor_list', anchors: @project.empty_repo_statistics_buttons - if can?(current_user, :push_code, @project) %div{ class: [container_class, ("limit-container-width" unless fluid_layout)] } @@ -42,7 +46,7 @@ .empty_wrapper %h3#repo-command-line-instructions.page-title-empty Command line instructions - .git-empty + .git-empty.js-git-empty %fieldset %h5 Git global setup %pre.bg-light @@ -54,7 +58,7 @@ %h5 Create a new repository %pre.bg-light :preserve - git clone #{ content_tag(:span, default_url_to_repo, class: 'clone')} + git clone #{ content_tag(:span, default_url_to_repo, class: 'js-clone')} cd #{h @project.path} touch README.md git add README.md @@ -69,7 +73,7 @@ :preserve cd existing_folder git init - git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')} + git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'js-clone')} git add . git commit -m "Initial commit" - if @project.can_current_user_push_to_default_branch? @@ -82,7 +86,7 @@ :preserve cd existing_repo git remote rename origin old-origin - git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')} + git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'js-clone')} - if @project.can_current_user_push_to_default_branch? %span>< git push -u origin --all diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index df8a5742450..aba289c790f 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -19,8 +19,13 @@ - if can?(current_user, :download_code, @project) %nav.project-stats{ class: [container_class, ("limit-container-width" unless fluid_layout)] } - = render 'stat_anchor_list', anchors: @project.statistics_anchors(show_auto_devops_callout: show_auto_devops_callout) - = render 'stat_anchor_list', anchors: @project.statistics_buttons(show_auto_devops_callout: show_auto_devops_callout) + .scrolling-tabs-container.inner-page-scroll-tabs.is-smaller + .fade-left= icon('angle-left') + .fade-right= icon('angle-right') + .nav-links.scrolling-tabs + = render 'stat_anchor_list', anchors: @project.statistics_anchors(show_auto_devops_callout: show_auto_devops_callout) + = render 'stat_anchor_list', anchors: @project.statistics_buttons(show_auto_devops_callout: show_auto_devops_callout) + = repository_languages_bar(@project.repository_languages) %div{ class: [container_class, ("limit-container-width" unless fluid_layout)] } diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml index de692466fe5..7fb80450161 100644 --- a/app/views/projects/wikis/_form.html.haml +++ b/app/views/projects/wikis/_form.html.haml @@ -1,9 +1,13 @@ - commit_message = @page.persisted? ? s_("WikiPageEdit|Update %{page_title}") : s_("WikiPageCreate|Create %{page_title}") - commit_message = commit_message % { page_title: @page.title } +- if params[:legacy_render] || !commonmark_for_repositories_enabled? + - markdown_version = CacheMarkdownField::CACHE_REDCARPET_VERSION +- else + - markdown_version = 0 = form_for [@project.namespace.becomes(Namespace), @project, @page], method: @page.persisted? ? :put : :post, html: { class: 'wiki-form common-note-form prepend-top-default js-quick-submit' }, - data: { markdown_version: CacheMarkdownField::CACHE_REDCARPET_VERSION } do |f| + data: { markdown_version: markdown_version } do |f| = form_errors(@page) - if @page.persisted? diff --git a/app/views/projects/wikis/_sidebar.html.haml b/app/views/projects/wikis/_sidebar.html.haml index 28353927135..02c5a6ea55c 100644 --- a/app/views/projects/wikis/_sidebar.html.haml +++ b/app/views/projects/wikis/_sidebar.html.haml @@ -12,7 +12,7 @@ .blocks-container .block.block-first - if @sidebar_page - = render_wiki_content(@sidebar_page) + = render_wiki_content(@sidebar_page, legacy_render_context(params)) - else %ul.wiki-pages = render @sidebar_wiki_entries, context: 'sidebar' diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml index a08973c7f32..19b9744b508 100644 --- a/app/views/projects/wikis/show.html.haml +++ b/app/views/projects/wikis/show.html.haml @@ -26,6 +26,6 @@ .prepend-top-default.append-bottom-default .wiki - = render_wiki_content(@page) + = render_wiki_content(@page, legacy_render_context(params)) = render 'sidebar' diff --git a/app/views/search/results/_snippet_blob.html.haml b/app/views/search/results/_snippet_blob.html.haml index 57a0b64bfd5..8b95bdf9747 100644 --- a/app/views/search/results/_snippet_blob.html.haml +++ b/app/views/search/results/_snippet_blob.html.haml @@ -21,7 +21,7 @@ .file-content.wiki - snippet_chunks.each do |chunk| - unless chunk[:data].empty? - = markup(snippet.file_name, chunk[:data]) + = markup(snippet.file_name, chunk[:data], legacy_render_context(params)) - else .file-content.code .nothing-here-block Empty file diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml index 3655c2a1d42..a2df0347fd6 100644 --- a/app/views/shared/_clone_panel.html.haml +++ b/app/views/shared/_clone_panel.html.haml @@ -1,14 +1,14 @@ - project = project || @project -.git-clone-holder.input-group +.git-clone-holder.js-git-clone-holder.input-group .input-group-prepend - if allowed_protocols_present? .input-group-text.clone-dropdown-btn.btn - %span + %span.js-clone-dropdown-label = enabled_project_button(project, enabled_protocol) - else %a#clone-dropdown.input-group-text.btn.clone-dropdown-btn.qa-clone-dropdown{ href: '#', data: { toggle: 'dropdown' } } - %span + %span.js-clone-dropdown-label = default_clone_protocol.upcase = icon('caret-down') %ul.dropdown-menu.dropdown-menu-selectable.clone-options-dropdown diff --git a/app/views/shared/_mobile_clone_panel.html.haml b/app/views/shared/_mobile_clone_panel.html.haml new file mode 100644 index 00000000000..998985cabe1 --- /dev/null +++ b/app/views/shared/_mobile_clone_panel.html.haml @@ -0,0 +1,13 @@ +- project = project || @project +- ssh_copy_label = _("Copy SSH clone URL") +- http_copy_label = _("Copy HTTPS clone URL") + +.btn-group.mobile-git-clone.js-mobile-git-clone + = clipboard_button(button_text: default_clone_label, target: '#project_clone', hide_button_icon: true, class: "input-group-text clone-dropdown-btn js-clone-dropdown-label btn btn-default") + %button.btn.btn-default.dropdown-toggle.js-dropdown-toggle{ type: "button", data: { toggle: "dropdown" } } + = icon("caret-down", class: "dropdown-btn-icon") + %ul.dropdown-menu.dropdown-menu-selectable.dropdown-menu-right.clone-options-dropdown{ data: { dropdown: true } } + %li + = dropdown_item_with_description(ssh_copy_label, project.ssh_url_to_repo, href: project.ssh_url_to_repo, data: { clone_type: 'ssh' }) + %li + = dropdown_item_with_description(http_copy_label, project.http_url_to_repo, href: project.http_url_to_repo, data: { clone_type: 'http' }) diff --git a/app/views/shared/groups/_empty_state.html.haml b/app/views/shared/groups/_empty_state.html.haml index 13bb4baee3f..a9c78547eae 100644 --- a/app/views/shared/groups/_empty_state.html.haml +++ b/app/views/shared/groups/_empty_state.html.haml @@ -1,4 +1,4 @@ -.groups-empty-state +.groups-empty-state.qa-groups-empty-state = custom_icon("icon_empty_groups") .text-content diff --git a/app/views/shared/web_hooks/_form.html.haml b/app/views/shared/web_hooks/_form.html.haml index 07ebb8680d2..9c5b9593bba 100644 --- a/app/views/shared/web_hooks/_form.html.haml +++ b/app/views/shared/web_hooks/_form.html.haml @@ -17,6 +17,7 @@ %strong Push events %p.light.ml-1 This URL will be triggered by a push to the repository + = form.text_field :push_events_branch_filter, class: 'form-control', placeholder: 'Branch name or wildcard pattern to trigger on (leave blank for all)' %li = form.check_box :tag_push_events, class: 'form-check-input' = form.label :tag_push_events, class: 'list-label form-check-label ml-1' do diff --git a/changelogs/unreleased/29398-support-kubernetes-rbac-for-gitlab-managed-apps.yml b/changelogs/unreleased/29398-support-kubernetes-rbac-for-gitlab-managed-apps.yml new file mode 100644 index 00000000000..c182946b299 --- /dev/null +++ b/changelogs/unreleased/29398-support-kubernetes-rbac-for-gitlab-managed-apps.yml @@ -0,0 +1,5 @@ +--- +title: Support Kubernetes RBAC for GitLab Managed Apps when adding a existing cluster +merge_request: 21127 +author: +type: changed diff --git a/changelogs/unreleased/36534-show-commit-behind-mr-api.yml b/changelogs/unreleased/36534-show-commit-behind-mr-api.yml new file mode 100644 index 00000000000..06471146fa3 --- /dev/null +++ b/changelogs/unreleased/36534-show-commit-behind-mr-api.yml @@ -0,0 +1,5 @@ +--- +title: Adds diverged_commits_count field to GET api/v4/projects/:project_id/merge_requests/:merge_request_iid +merge_request: 21405 +author: Jacopo Beschi @jacopo-beschi +type: added diff --git a/changelogs/unreleased/44704-improve-project-overview-ui.yml b/changelogs/unreleased/44704-improve-project-overview-ui.yml new file mode 100644 index 00000000000..6fb8359f2dc --- /dev/null +++ b/changelogs/unreleased/44704-improve-project-overview-ui.yml @@ -0,0 +1,5 @@ +--- +title: Update design of project overview page +merge_request: 20536 +author: +type: changed diff --git a/changelogs/unreleased/44943-update-presentation-for-sso-providers-on-log-in-page.yml b/changelogs/unreleased/44943-update-presentation-for-sso-providers-on-log-in-page.yml new file mode 100644 index 00000000000..a378aaec750 --- /dev/null +++ b/changelogs/unreleased/44943-update-presentation-for-sso-providers-on-log-in-page.yml @@ -0,0 +1,5 @@ +--- +title: Update presentation for SSO providers on log in page +merge_request: 21233 +author: +type: other diff --git a/changelogs/unreleased/45938-postgres-timeout-when-counting-number-of-ci-builds-for-usage-ping.yml b/changelogs/unreleased/45938-postgres-timeout-when-counting-number-of-ci-builds-for-usage-ping.yml new file mode 100644 index 00000000000..f3016a639d9 --- /dev/null +++ b/changelogs/unreleased/45938-postgres-timeout-when-counting-number-of-ci-builds-for-usage-ping.yml @@ -0,0 +1,5 @@ +--- +title: Handle database statement timeouts in usage ping +merge_request: 21523 +author: +type: fixed diff --git a/changelogs/unreleased/47440-recognize-unlicense-license-file.yml b/changelogs/unreleased/47440-recognize-unlicense-license-file.yml new file mode 100644 index 00000000000..3521dd613b0 --- /dev/null +++ b/changelogs/unreleased/47440-recognize-unlicense-license-file.yml @@ -0,0 +1,5 @@ +--- +title: Recognize 'UNLICENSE' license files +merge_request: 21508 +author: J.D. Bean +type: added diff --git a/changelogs/unreleased/50567-remove-usage-ping-payload-from-cohorts-add-to-settings.yml b/changelogs/unreleased/50567-remove-usage-ping-payload-from-cohorts-add-to-settings.yml new file mode 100644 index 00000000000..2a6666e362c --- /dev/null +++ b/changelogs/unreleased/50567-remove-usage-ping-payload-from-cohorts-add-to-settings.yml @@ -0,0 +1,5 @@ +--- +title: Move usage ping payload from User Cohorts page to admin application settings +merge_request: 21343 +author: +type: other diff --git a/changelogs/unreleased/51092-fix-mr-diff-file-filter-clear-button.yml b/changelogs/unreleased/51092-fix-mr-diff-file-filter-clear-button.yml new file mode 100644 index 00000000000..cb5ab15d285 --- /dev/null +++ b/changelogs/unreleased/51092-fix-mr-diff-file-filter-clear-button.yml @@ -0,0 +1,5 @@ +--- +title: Make MR diff file filter input Clear button functional +merge_request: 21556 +author: +type: fixed diff --git a/changelogs/unreleased/51117-send-terminal-path-in-job-api.yml b/changelogs/unreleased/51117-send-terminal-path-in-job-api.yml new file mode 100644 index 00000000000..f6faa9549be --- /dev/null +++ b/changelogs/unreleased/51117-send-terminal-path-in-job-api.yml @@ -0,0 +1,5 @@ +--- +title: Add terminal_path to job API response +merge_request: 21537 +author: +type: other diff --git a/changelogs/unreleased/an-api-route-logger.yml b/changelogs/unreleased/an-api-route-logger.yml new file mode 100644 index 00000000000..cca3ef44f36 --- /dev/null +++ b/changelogs/unreleased/an-api-route-logger.yml @@ -0,0 +1,5 @@ +--- +title: Add route information to lograge structured logging for API logs +merge_request: 21487 +author: +type: other diff --git a/changelogs/unreleased/ash-mckenzie-geo-git-push-ssh-proxy.yml b/changelogs/unreleased/ash-mckenzie-geo-git-push-ssh-proxy.yml new file mode 100644 index 00000000000..c145c744cef --- /dev/null +++ b/changelogs/unreleased/ash-mckenzie-geo-git-push-ssh-proxy.yml @@ -0,0 +1,5 @@ +--- +title: 'Support a custom action, such as proxying to another server, after /api/v4/internal/allowed check succeeds' +merge_request: 21034 +author: +type: changed diff --git a/changelogs/unreleased/bw-commonmark-for-files.yml b/changelogs/unreleased/bw-commonmark-for-files.yml new file mode 100644 index 00000000000..f932ccb704f --- /dev/null +++ b/changelogs/unreleased/bw-commonmark-for-files.yml @@ -0,0 +1,5 @@ +--- +title: Render files (`.md`) and wikis using CommonMark +merge_request: 21228 +author: +type: changed diff --git a/changelogs/unreleased/feature-gb-allow-to-extend-keys-in-gitlab-ci-yml.yml b/changelogs/unreleased/feature-gb-allow-to-extend-keys-in-gitlab-ci-yml.yml new file mode 100644 index 00000000000..b46dfd47e7a --- /dev/null +++ b/changelogs/unreleased/feature-gb-allow-to-extend-keys-in-gitlab-ci-yml.yml @@ -0,0 +1,5 @@ +--- +title: Add support for extendable CI/CD config with +merge_request: 21243 +author: +type: added diff --git a/changelogs/unreleased/feature-git-v2-flag.yml b/changelogs/unreleased/feature-git-v2-flag.yml new file mode 100644 index 00000000000..c105c477666 --- /dev/null +++ b/changelogs/unreleased/feature-git-v2-flag.yml @@ -0,0 +1,5 @@ +--- +title: Add git_v2 feature flag +merge_request: 21520 +author: +type: added diff --git a/changelogs/unreleased/filter-web-hooks-by-branch.yml b/changelogs/unreleased/filter-web-hooks-by-branch.yml new file mode 100644 index 00000000000..7bd2c191d7f --- /dev/null +++ b/changelogs/unreleased/filter-web-hooks-by-branch.yml @@ -0,0 +1,5 @@ +--- +title: Add branch filter to project webhooks +merge_request: 20338 +author: Duana Saskia +type: added diff --git a/changelogs/unreleased/monaco-upgrade.yml b/changelogs/unreleased/monaco-upgrade.yml new file mode 100644 index 00000000000..ec441ba182b --- /dev/null +++ b/changelogs/unreleased/monaco-upgrade.yml @@ -0,0 +1,5 @@ +--- +title: Upgrade Monaco editor +merge_request: +author: +type: other diff --git a/changelogs/unreleased/rails5-explicit-hashed-path-check.yml b/changelogs/unreleased/rails5-explicit-hashed-path-check.yml new file mode 100644 index 00000000000..93fe4a38411 --- /dev/null +++ b/changelogs/unreleased/rails5-explicit-hashed-path-check.yml @@ -0,0 +1,6 @@ +--- +title: Explicit hashed path check for trace, prevents background migration from accessing + file_location column that doesn't exist +merge_request: 21533 +author: Jasper Maes +type: other diff --git a/changelogs/unreleased/sh-add-ua-to-lograge-logs.yml b/changelogs/unreleased/sh-add-ua-to-lograge-logs.yml new file mode 100644 index 00000000000..eec55bd3a24 --- /dev/null +++ b/changelogs/unreleased/sh-add-ua-to-lograge-logs.yml @@ -0,0 +1,5 @@ +--- +title: Add User-Agent to production_json.log +merge_request: 21546 +author: +type: other diff --git a/changelogs/unreleased/sh-fix-attachments-inline.yml b/changelogs/unreleased/sh-fix-attachments-inline.yml new file mode 100644 index 00000000000..2926edca97a --- /dev/null +++ b/changelogs/unreleased/sh-fix-attachments-inline.yml @@ -0,0 +1,5 @@ +--- +title: Fix attachments not displaying inline with Google Cloud Storage +merge_request: 21265 +author: +type: fixed diff --git a/changelogs/unreleased/skip-irrelevant-sql-commands-in-metrics.yml b/changelogs/unreleased/skip-irrelevant-sql-commands-in-metrics.yml new file mode 100644 index 00000000000..56d236d0029 --- /dev/null +++ b/changelogs/unreleased/skip-irrelevant-sql-commands-in-metrics.yml @@ -0,0 +1,5 @@ +--- +title: Ignore irrelevant sql commands in metrics +merge_request: 21498 +author: +type: other diff --git a/config/initializers/carrierwave_patch.rb b/config/initializers/carrierwave_patch.rb new file mode 100644 index 00000000000..35ffff03abe --- /dev/null +++ b/config/initializers/carrierwave_patch.rb @@ -0,0 +1,29 @@ +# This monkey patches CarrierWave 1.2.3 to make Google Cloud Storage work with +# extra query parameters: +# https://github.com/carrierwaveuploader/carrierwave/pull/2332/files +module CarrierWave + module Storage + class Fog < Abstract + class File + def authenticated_url(options = {}) + if %w(AWS Google Rackspace OpenStack).include?(@uploader.fog_credentials[:provider]) + # avoid a get by using local references + local_directory = connection.directories.new(key: @uploader.fog_directory) + local_file = local_directory.files.new(key: path) + expire_at = ::Fog::Time.now + @uploader.fog_authenticated_url_expiration + case @uploader.fog_credentials[:provider] + when 'AWS', 'Google' + local_file.url(expire_at, options) + when 'Rackspace' + connection.get_object_https_url(@uploader.fog_directory, path, expire_at, options) + when 'OpenStack' + connection.get_object_https_url(@uploader.fog_directory, path, expire_at) + else + local_file.url(expire_at) + end + end + end + end + end + end +end diff --git a/config/initializers/fog_google_https_private_urls.rb b/config/initializers/fog_google_https_private_urls.rb index c65a534b536..682b1050c68 100644 --- a/config/initializers/fog_google_https_private_urls.rb +++ b/config/initializers/fog_google_https_private_urls.rb @@ -9,7 +9,7 @@ module Fog module MonkeyPatch def url(expires, options = {}) requires :key - collection.get_https_url(key, expires) + collection.get_https_url(key, expires, options) end end diff --git a/config/initializers/lograge.rb b/config/initializers/lograge.rb index 1cf8a24e98c..840404e0ec0 100644 --- a/config/initializers/lograge.rb +++ b/config/initializers/lograge.rb @@ -22,7 +22,8 @@ unless Sidekiq.server? params: params, remote_ip: event.payload[:remote_ip], user_id: event.payload[:user_id], - username: event.payload[:username] + username: event.payload[:username], + ua: event.payload[:ua] } gitaly_calls = Gitlab::GitalyClient.get_request_count diff --git a/db/migrate/20180607071808_add_push_events_branch_filter_to_web_hooks.rb b/db/migrate/20180607071808_add_push_events_branch_filter_to_web_hooks.rb new file mode 100644 index 00000000000..6a69460e611 --- /dev/null +++ b/db/migrate/20180607071808_add_push_events_branch_filter_to_web_hooks.rb @@ -0,0 +1,12 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddPushEventsBranchFilterToWebHooks < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + add_column :web_hooks, :push_events_branch_filter, :text + end +end diff --git a/db/migrate/20180815040323_add_authorization_type_to_cluster_platforms_kubernetes.rb b/db/migrate/20180815040323_add_authorization_type_to_cluster_platforms_kubernetes.rb new file mode 100644 index 00000000000..6397d6dd99f --- /dev/null +++ b/db/migrate/20180815040323_add_authorization_type_to_cluster_platforms_kubernetes.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class AddAuthorizationTypeToClusterPlatformsKubernetes < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def change + add_column :cluster_platforms_kubernetes, :authorization_type, :integer, limit: 2 + end +end diff --git a/db/schema.rb b/db/schema.rb index 56c7265119d..1d05be0d3e8 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -588,6 +588,7 @@ ActiveRecord::Schema.define(version: 20180826111825) do t.string "encrypted_password_iv" t.text "encrypted_token" t.string "encrypted_token_iv" + t.integer "authorization_type", limit: 2 end add_index "cluster_platforms_kubernetes", ["cluster_id"], name: "index_cluster_platforms_kubernetes_on_cluster_id", unique: true, using: :btree @@ -2245,6 +2246,7 @@ ActiveRecord::Schema.define(version: 20180826111825) do t.boolean "repository_update_events", default: false, null: false t.boolean "job_events", default: false, null: false t.boolean "confidential_note_events" + t.text "push_events_branch_filter" end add_index "web_hooks", ["project_id"], name: "index_web_hooks_on_project_id", using: :btree diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index 58d05a70d05..9e6676d62fe 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -352,6 +352,7 @@ Parameters: - `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user - `merge_request_iid` (required) - The internal ID of the merge request - `render_html` (optional) - If `true` response includes rendered HTML for title and description +- `include_diverged_commits_count` (optional) - If `true` response includes the commits behind the target branch ```json { @@ -435,7 +436,8 @@ Parameters: "username" : "root", "id" : 1, "name" : "Administrator" - } + }, + "diverged_commits_count": 2 } ``` diff --git a/doc/api/projects.md b/doc/api/projects.md index 86acb96357d..7e8b7c4b502 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -1334,6 +1334,7 @@ GET /projects/:id/hooks/:hook_id "url": "http://example.com/hook", "project_id": 3, "push_events": true, + "push_events_branch_filter": "", "issues_events": true, "confidential_issues_events": true, "merge_requests_events": true, @@ -1360,6 +1361,7 @@ POST /projects/:id/hooks | `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) | | `url` | string | yes | The hook URL | | `push_events` | boolean | no | Trigger hook on push events | +| `push_events_branch_filter` | string | no | Trigger hook on push events for matching branches only | | `issues_events` | boolean | no | Trigger hook on issues events | | `confidential_issues_events` | boolean | no | Trigger hook on confidential issues events | | `merge_requests_events` | boolean | no | Trigger hook on merge requests events | @@ -1385,6 +1387,7 @@ PUT /projects/:id/hooks/:hook_id | `hook_id` | integer | yes | The ID of the project hook | | `url` | string | yes | The hook URL | | `push_events` | boolean | no | Trigger hook on push events | +| `push_events_branch_filter` | string | no | Trigger hook on push events for matching branches only | | `issues_events` | boolean | no | Trigger hook on issues events | | `confidential_issues_events` | boolean | no | Trigger hook on confidential issues events | | `merge_requests_events` | boolean | no | Trigger hook on merge requests events | diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md index 47e658f610e..636117536a2 100644 --- a/doc/ci/quick_start/README.md +++ b/doc/ci/quick_start/README.md @@ -7,7 +7,7 @@ projects. GitLab offers a [continuous integration][ci] service. If you [add a `.gitlab-ci.yml` file][yaml] to the root directory of your repository, and configure your GitLab project to use a [Runner], then each commit or -push, triggers your CI [pipeline]. +push triggers your CI [pipeline]. The `.gitlab-ci.yml` file tells the GitLab runner what to do. By default it runs a pipeline with three [stages]: `build`, `test`, and `deploy`. You don't need to diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index e93060fec85..c1ebe39e076 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -56,6 +56,7 @@ A job is defined by a list of parameters that define the job behavior. | Keyword | Required | Description | |---------------|----------|-------------| | script | yes | Defines a shell script which is executed by Runner | +| extends | no | Defines a configuration entry that this job is going to inherit from | | image | no | Use docker image, covered in [Using Docker Images](../docker/using_docker_images.md#define-image-and-services-from-gitlab-ciyml) | | services | no | Use docker services, covered in [Using Docker Images](../docker/using_docker_images.md#define-image-and-services-from-gitlab-ciyml) | | stage | no | Defines a job stage (default: `test`) | @@ -75,6 +76,79 @@ A job is defined by a list of parameters that define the job behavior. | coverage | no | Define code coverage settings for a given job | | retry | no | Define how many times a job can be auto-retried in case of a failure | +### `extends` + +> Introduced in GitLab 11.3 + +`extends` defines an entry name that a job, that uses `extends` is going to +inherit from. + +`extends` in an alternative to using [YAML anchors](#anchors) that is a little +more flexible and readable. + +```yaml +.tests: + only: + refs: + - branches + +rspec: + extends: .tests + script: rake rspec + stage: test + only: + variables: + - $RSPEC +``` + +In the example above the `rspec` job is going to inherit from `.tests` +template. GitLab will perform a reverse deep merge, what means that it will +merge `rspec` contents into `.tests` recursively, and it is going to result in +following configuration of the `rspec` job: + +```yaml +rspec: + script: rake rspec + stage: test + only: + refs: + - branches + variables: + - $RSPEC +``` + +`.tests` in this example is a [hidden key](#hidden-keys-jobs), but it is +possible to inherit from regular jobs as well. + +`extends` supports multi-level inheritance, however it is not recommended to +use more than three levels of inheritance. Maximum nesting level supported is +10 levels. + + +```yaml +.tests: + only: + - pushes + +.rspec: + extends: .tests + script: rake rspec + +rspec 1: + variables: + RSPEC_SUITE: '1' + extends: .rspec + +rspec 2: + variables: + RSPEC_SUITE: '2' + extends: .rspec + +spinach: + extends: .tests + script: rake spinach +``` + ### `pages` `pages` is a special job that is used to upload static content to GitLab that diff --git a/doc/development/feature_flags.md b/doc/development/feature_flags.md index 702caacc74f..6f757f1ce7b 100644 --- a/doc/development/feature_flags.md +++ b/doc/development/feature_flags.md @@ -58,13 +58,20 @@ Features that are developed and are intended to be merged behind a feature flag should not include a changelog entry. The entry should be added in the merge request removing the feature flags. +In the rare case that you need the feature flag to be on automatically, use +`default_enabled: true` when checking: + +```ruby +Feature.enabled?(:feature_flag, project, default_enabled: true) +``` + ### Specs In the test environment `Feature.enabled?` is stubbed to always respond to `true`, so we make sure behavior under feature flag doesn't go untested in some non-specific contexts. - -If you need to test the feature flag in a different state, you need to stub it with: + +If you need to test the feature flag in a different state, you need to stub it with: ```ruby stub_feature_flags(my_feature_flag: false) diff --git a/doc/install/kubernetes/gitlab_chart.md b/doc/install/kubernetes/gitlab_chart.md index 8852570b254..cd380b1dd01 100644 --- a/doc/install/kubernetes/gitlab_chart.md +++ b/doc/install/kubernetes/gitlab_chart.md @@ -133,6 +133,7 @@ Once your GitLab Chart is installed, configuration changes and chart updates should be done using `helm upgrade`: ```sh +helm repo update helm upgrade --reuse-values gitlab gitlab/gitlab ``` diff --git a/doc/user/admin_area/settings/usage_statistics.md b/doc/user/admin_area/settings/usage_statistics.md index b7427592e10..35a9d7adb28 100644 --- a/doc/user/admin_area/settings/usage_statistics.md +++ b/doc/user/admin_area/settings/usage_statistics.md @@ -23,7 +23,7 @@ GitLab Inc. collects your instance's version and hostname (through the HTTP referer) as part of the version check. No other information is collected. This information is used, among other things, to identify to which versions -patches will need to be back ported, making sure active GitLab instances remain +patches will need to be backported, making sure active GitLab instances remain secure. If you disable version check, this information will not be collected. Enable or @@ -33,7 +33,8 @@ disable the version check at **Admin area > Settings > Usage statistics**. > [Introduced][ee-557] in GitLab Enterprise Edition 8.10. More statistics [were added][ee-735] in GitLab Enterprise Edition -8.12. [Moved to GitLab Community Edition][ce-23361] in 9.1. +8.12. [Moved to GitLab Core][ce-23361] in 9.1. More statistics +[were added][ee-6602] in GitLab Ultimate 11.2. GitLab sends a weekly payload containing usage data to GitLab Inc. The usage ping uses high-level data to help our product, support, and sales teams. It does @@ -79,3 +80,4 @@ Statistics visibility section under **Admin area > Settings > Usage statistics** [ee-557]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/557 [ee-735]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/735 [ce-23361]: https://gitlab.com/gitlab-org/gitlab-ce/issues/23361 +[ee-6602]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/6602 diff --git a/doc/user/instance_statistics/index.md b/doc/user/instance_statistics/index.md index a4eca89b7fe..22f76f728e3 100644 --- a/doc/user/instance_statistics/index.md +++ b/doc/user/instance_statistics/index.md @@ -10,9 +10,6 @@ and can be accessed via the top bar. ![Instance Statistics button](img/instance_statistics_button.png) -For the statistics to show up, [usage ping must be enabled](../admin_area/settings/usage_statistics.md#usage-ping) -by an admin in the admin settings area. - There are two kinds of statistics: - [Conversational Development (ConvDev) Index](convdev.md): Provides an overview of your entire instance's feature usage. diff --git a/doc/user/markdown.md b/doc/user/markdown.md index 6203561265b..e9c8980a3e6 100644 --- a/doc/user/markdown.md +++ b/doc/user/markdown.md @@ -7,13 +7,13 @@ > this document currently work on our documentation website. > > For the best result, we encourage you to check this document out as rendered -by GitLab: [markdown.md] +> by GitLab: [markdown.md] -_GitLab uses (as of 11.1) the [CommonMark Ruby Library][commonmarker] for Markdown processing of all new issues, merge requests, comments, and other Markdown content in the GitLab system. Previous content, wiki pages and Markdown files (`.md`) in the repositories are still processed using the [Redcarpet Ruby library][redcarpet]._ +_GitLab uses (as of 11.1) the [CommonMark Ruby Library][commonmarker] for Markdown processing of all new issues, merge requests, comments, and other Markdown content in the GitLab system. As of 11.3, wiki pages and Markdown files (`.md`) in the repositories are also processed with CommonMark. Older content in issues/comments are still processed using the [Redcarpet Ruby library][redcarpet]._ _Where there are significant differences, we will try to call them out in this document._ -GitLab uses "GitLab Flavored Markdown" (GFM). It extends the standard Markdown in a few significant ways to add some useful functionality. It was inspired by [GitHub Flavored Markdown](https://help.github.com/articles/basic-writing-and-formatting-syntax/). +GitLab uses "GitLab Flavored Markdown" (GFM). It extends the [CommonMark specification][commonmark-spec] (which is based on standard Markdown) in a few significant ways to add some useful functionality. It was inspired by [GitHub Flavored Markdown](https://help.github.com/articles/basic-writing-and-formatting-syntax/). You can use GFM in the following areas: @@ -22,18 +22,46 @@ You can use GFM in the following areas: - merge requests - milestones - snippets (the snippet must be named with a `.md` extension) -- wiki pages (currently only rendered by Redcarpet) -- markdown documents inside the repository (currently only rendered by Redcarpet) +- wiki pages +- markdown documents inside the repository You can also use other rich text files in GitLab. You might have to install a dependency to do so. Please see the [github-markup gem readme](https://github.com/gitlabhq/markup#markups) for more information. +### Transitioning to CommonMark + +You may have Markdown documents in your repository that were written using some of the nuances of RedCarpet's version of Markdown. Since CommonMark uses a slightly stricter syntax, these documents may now display a little strangely since we've transitioned to CommonMark. Numbered lists with nested lists in particular can be displayed incorrectly. + +It is usually quite easy to fix. In the case of a nested list such as this: + +```markdown +1. Chocolate + - dark + - milk +``` + +simply add a space to each nested item: + +```markdown +1. Chocolate + - dark + - milk +``` + +In the documentation below, we try to highlight some of the differences. + +If you have a need to view a document using RedCarpet, you can add the token `legacy_render=1` to the end of the url, like this: + +https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md?legacy_render=1 + +If you have a large volume of Markdown files, it can be tedious to determine if they will be displayed correctly or not. You can use the [diff_redcarpet_cmark](https://gitlab.com/digitalmoksha/diff_redcarpet_cmark) tool (not an officially supported product) to generate a list of files and differences between how RedCarpet and CommonMark render the files. It can give you a great idea if anything needs to be changed - many times nothing will need to changed. + ### Newlines > If this is not rendered correctly, see https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#newlines -GFM honors the markdown specification in how [paragraphs and line breaks are handled](https://daringfireball.net/projects/markdown/syntax#p). +GFM honors the markdown specification in how [paragraphs and line breaks are handled][commonmark-spec]. A paragraph is simply one or more consecutive lines of text, separated by one or more blank lines. Line-breaks, or soft returns, are rendered if you end a line with two or more spaces: @@ -979,8 +1007,9 @@ A link starting with a `/` is relative to the wiki root. ## References - This document leveraged heavily from the [Markdown-Cheatsheet](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet). -- The [Markdown Syntax Guide](https://daringfireball.net/projects/markdown/syntax) at Daring Fireball is an excellent resource for a detailed explanation of standard markdown. -- [Dillinger.io](http://dillinger.io) is a handy tool for testing standard markdown. +- The original [Markdown Syntax Guide](https://daringfireball.net/projects/markdown/syntax) at Daring Fireball is an excellent resource for a detailed explanation of standard markdown. +- The detailed specification for CommonMark can be found in the [CommonMark Spec][commonmark-spec] +- The [CommonMark Dingus](http://try.commonmark.org) is a handy tool for testing CommonMark syntax. [^1]: This link will be broken if you see this document from the Help page or docs.gitlab.com [^2]: This is my awesome footnote. @@ -993,3 +1022,4 @@ A link starting with a `/` is relative to the wiki root. [katex-subset]: https://github.com/Khan/KaTeX/wiki/Function-Support-in-KaTeX "Macros supported by KaTeX" [asciidoctor-manual]: http://asciidoctor.org/docs/user-manual/#activating-stem-support "Asciidoctor user manual" [commonmarker]: https://github.com/gjtorikian/commonmarker +[commonmark-spec]: https://spec.commonmark.org/current/ diff --git a/doc/user/project/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md index 770b1810da1..a64e080d6b7 100644 --- a/doc/user/project/integrations/webhooks.md +++ b/doc/user/project/integrations/webhooks.md @@ -65,8 +65,8 @@ Below are described the supported events. Triggered when you push to the repository except when pushing tags. -> **Note:** When more than 20 commits are pushed at once, the `commits` web hook - attribute will only contain the first 20 for performance reasons. Loading +> **Note:** When more than 20 commits are pushed at once, the `commits` web hook + attribute will only contain the first 20 for performance reasons. Loading detailed commit data is expensive. Note that despite only 20 commits being present in the `commits` attribute, the `total_commits_count` attribute will contain the actual total. diff --git a/lib/api/api.rb b/lib/api/api.rb index 850cef26449..843f75d3096 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -15,6 +15,7 @@ module API include: [ GrapeLogging::Loggers::FilterParameters.new, GrapeLogging::Loggers::ClientEnv.new, + Gitlab::GrapeLogging::Loggers::RouteLogger.new, Gitlab::GrapeLogging::Loggers::UserLogger.new, Gitlab::GrapeLogging::Loggers::QueueDurationLogger.new, Gitlab::GrapeLogging::Loggers::PerfLogger.new diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 624eda3f5dd..90abee94f6a 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -105,6 +105,7 @@ module API expose :project_id, :issues_events, :confidential_issues_events expose :note_events, :confidential_note_events, :pipeline_events, :wiki_page_events expose :job_events + expose :push_events_branch_filter end class SharedGroup < Grape::Entity @@ -687,6 +688,8 @@ module API expose :diff_refs, using: Entities::DiffRefs + expose :diverged_commits_count, as: :diverged_commits_count, if: -> (_, options) { options[:include_diverged_commits_count] } + def build_available?(options) options[:project]&.feature_available?(:builds, options[:current_user]) end diff --git a/lib/api/internal.rb b/lib/api/internal.rb index 516f25db15b..0990e2a1fba 100644 --- a/lib/api/internal.rb +++ b/lib/api/internal.rb @@ -6,8 +6,17 @@ module API helpers ::API::Helpers::InternalHelpers helpers ::Gitlab::Identifier + UNKNOWN_CHECK_RESULT_ERROR = 'Unknown check result'.freeze + + helpers do + def response_with_status(code: 200, success: true, message: nil, **extra_options) + status code + { status: success, message: message }.merge(extra_options).compact + end + end + namespace 'internal' do - # Check if git command is allowed to project + # Check if git command is allowed for project # # Params: # key_id - ssh key id for Git over SSH @@ -18,8 +27,6 @@ module API # action - git action (git-upload-pack or git-receive-pack) # changes - changes as "oldrev newrev ref", see Gitlab::ChangesList post "/allowed" do - status 200 - # Stores some Git-specific env thread-safely env = parse_env Gitlab::Git::HookEnv.set(gl_repository, env) if project @@ -49,27 +56,37 @@ module API namespace_path: namespace_path, project_path: project_path, redirected_path: redirected_path) - begin - access_checker.check(params[:action], params[:changes]) - @project ||= access_checker.project - rescue Gitlab::GitAccess::UnauthorizedError, Gitlab::GitAccess::NotFoundError => e - break { status: false, message: e.message } - end + check_result = begin + result = access_checker.check(params[:action], params[:changes]) + @project ||= access_checker.project + result + rescue Gitlab::GitAccess::UnauthorizedError => e + break response_with_status(code: 401, success: false, message: e.message) + rescue Gitlab::GitAccess::NotFoundError => e + break response_with_status(code: 404, success: false, message: e.message) + end log_user_activity(actor) - { - status: true, - gl_repository: gl_repository, - gl_id: Gitlab::GlId.gl_id(user), - gl_username: user&.username, - - # This repository_path is a bogus value but gitlab-shell still requires - # its presence. https://gitlab.com/gitlab-org/gitlab-shell/issues/135 - repository_path: '/', - - gitaly: gitaly_payload(params[:action]) - } + case check_result + when ::Gitlab::GitAccessResult::Success + payload = { + gl_repository: gl_repository, + gl_id: Gitlab::GlId.gl_id(user), + gl_username: user&.username, + + # This repository_path is a bogus value but gitlab-shell still requires + # its presence. https://gitlab.com/gitlab-org/gitlab-shell/issues/135 + repository_path: '/', + + gitaly: gitaly_payload(params[:action]) + } + response_with_status(**payload) + when ::Gitlab::GitAccessResult::CustomAction + response_with_status(code: 300, message: check_result.message, payload: check_result.payload) + else + response_with_status(code: 500, success: false, message: UNKNOWN_CHECK_RESULT_ERROR) + end end post "/lfs_authenticate" do diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index abad418771c..55f54fe3c43 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -233,6 +233,7 @@ module API params do requires :merge_request_iid, type: Integer, desc: 'The IID of a merge request' optional :render_html, type: Boolean, desc: 'Returns the description and title rendered HTML' + optional :include_diverged_commits_count, type: Boolean, desc: 'Returns the commits count behind the target branch' end desc 'Get a single merge request' do success Entities::MergeRequest @@ -240,7 +241,7 @@ module API get ':id/merge_requests/:merge_request_iid' do merge_request = find_merge_request_with_access(params[:merge_request_iid]) - present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project, render_html: params[:render_html] + present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project, render_html: params[:render_html], include_diverged_commits_count: params[:include_diverged_commits_count] end desc 'Get the participants of a merge request' do diff --git a/lib/api/project_hooks.rb b/lib/api/project_hooks.rb index 4760a1c08d7..0fb454bc22e 100644 --- a/lib/api/project_hooks.rb +++ b/lib/api/project_hooks.rb @@ -20,6 +20,7 @@ module API optional :wiki_page_events, type: Boolean, desc: "Trigger hook on wiki events" optional :enable_ssl_verification, type: Boolean, desc: "Do SSL verification when triggering the hook" optional :token, type: String, desc: "Secret token to validate received payloads; this will not be returned in the response" + optional :push_events_branch_filter, type: String, desc: "Trigger hook on specified branch only" end end @@ -63,6 +64,7 @@ module API present hook, with: Entities::ProjectHook else error!("Invalid url given", 422) if hook.errors[:url].present? + error!("Invalid branch filter given", 422) if hook.errors[:push_events_branch_filter].present? not_found!("Project hook #{hook.errors.messages}") end @@ -84,6 +86,7 @@ module API present hook, with: Entities::ProjectHook else error!("Invalid url given", 422) if hook.errors[:url].present? + error!("Invalid branch filter given", 422) if hook.errors[:push_events_branch_filter].present? not_found!("Project hook #{hook.errors.messages}") end diff --git a/lib/api/templates.rb b/lib/api/templates.rb index 927baaea652..7bf0e0f5934 100644 --- a/lib/api/templates.rb +++ b/lib/api/templates.rb @@ -4,15 +4,12 @@ module API GLOBAL_TEMPLATE_TYPES = { gitignores: { - klass: Gitlab::Template::GitignoreTemplate, gitlab_version: 8.8 }, gitlab_ci_ymls: { - klass: Gitlab::Template::GitlabCiYmlTemplate, gitlab_version: 8.9 }, dockerfiles: { - klass: Gitlab::Template::DockerfileTemplate, gitlab_version: 8.15 } }.freeze @@ -36,7 +33,7 @@ module API popular = declared(params)[:popular] popular = to_boolean(popular) if popular.present? - templates = LicenseTemplateFinder.new(popular: popular).execute + templates = TemplateFinder.build(:licenses, popular: popular).execute present paginate(::Kaminari.paginate_array(templates)), with: ::API::Entities::License end @@ -49,7 +46,7 @@ module API requires :name, type: String, desc: 'The name of the template' end get "templates/licenses/:name", requirements: { name: /[\w\.-]+/ } do - templates = LicenseTemplateFinder.new.execute + templates = TemplateFinder.build(:licenses).execute template = templates.find { |template| template.key == params[:name] } not_found!('License') unless template.present? @@ -63,7 +60,6 @@ module API end GLOBAL_TEMPLATE_TYPES.each do |template_type, properties| - klass = properties[:klass] gitlab_version = properties[:gitlab_version] desc 'Get the list of the available template' do @@ -74,7 +70,7 @@ module API use :pagination end get "templates/#{template_type}" do - templates = ::Kaminari.paginate_array(klass.all) + templates = ::Kaminari.paginate_array(TemplateFinder.new(template_type).execute) present paginate(templates), with: Entities::TemplatesList end @@ -86,7 +82,8 @@ module API requires :name, type: String, desc: 'The name of the template' end get "templates/#{template_type}/:name" do - new_template = klass.find(declared(params)[:name]) + finder = TemplateFinder.build(template_type, name: declared(params)[:name]) + new_template = finder.execute render_response(template_type, new_template) end diff --git a/lib/feature.rb b/lib/feature.rb index 24dbcb32fc0..f4b57376313 100644 --- a/lib/feature.rb +++ b/lib/feature.rb @@ -42,13 +42,21 @@ class Feature persisted_names.include?(feature.name.to_s) end - def enabled?(key, thing = nil) - get(key).enabled?(thing) + # use `default_enabled: true` to default the flag to being `enabled` + # unless set explicitly. The default is `disabled` + def enabled?(key, thing = nil, default_enabled: false) + feature = Feature.get(key) + + # If we're not default enabling the flag or the feature has been set, always evaluate. + # `persisted?` can potentially generate DB queries and also checks for inclusion + # in an array of feature names (177 at last count), possibly reducing performance by half. + # So we only perform the `persisted` check if `default_enabled: true` + !default_enabled || Feature.persisted?(feature) ? feature.enabled?(thing) : true end - def disabled?(key, thing = nil) + def disabled?(key, thing = nil, default_enabled: false) # we need to make different method calls to make it easy to mock / define expectations in test mode - thing.nil? ? !enabled?(key) : !enabled?(key, thing) + thing.nil? ? !enabled?(key, default_enabled: default_enabled) : !enabled?(key, thing, default_enabled: default_enabled) end def enable(key, thing = true) diff --git a/lib/gitlab/ci/config.rb b/lib/gitlab/ci/config.rb index 66ac4a40616..46dad59eb8c 100644 --- a/lib/gitlab/ci/config.rb +++ b/lib/gitlab/ci/config.rb @@ -4,12 +4,17 @@ module Gitlab # Base GitLab CI Configuration facade # class Config - # EE would override this and utilize opts argument + ConfigError = Class.new(StandardError) + def initialize(config, opts = {}) - @config = Loader.new(config).load! + @config = Config::Extendable + .new(build_config(config, opts)) + .to_hash @global = Entry::Global.new(@config) @global.compose! + rescue Loader::FormatError, Extendable::ExtensionError => e + raise Config::ConfigError, e.message end def valid? @@ -58,6 +63,11 @@ module Gitlab def jobs @global.jobs_value end + + # 'opts' argument is used in EE see /ee/lib/ee/gitlab/ci/config.rb + def build_config(config, opts = {}) + Loader.new(config).load! + end end end end diff --git a/lib/gitlab/ci/config/entry/job.rb b/lib/gitlab/ci/config/entry/job.rb index 91aac6df4b1..016a896bde5 100644 --- a/lib/gitlab/ci/config/entry/job.rb +++ b/lib/gitlab/ci/config/entry/job.rb @@ -9,9 +9,10 @@ module Gitlab include Configurable include Attributable - ALLOWED_KEYS = %i[tags script only except type image services allow_failure - type stage when artifacts cache dependencies before_script - after_script variables environment coverage retry].freeze + ALLOWED_KEYS = %i[tags script only except type image services + allow_failure type stage when artifacts cache + dependencies before_script after_script variables + environment coverage retry extends].freeze validations do validates :config, allowed_keys: ALLOWED_KEYS @@ -32,6 +33,7 @@ module Gitlab 'always or manual' } validates :dependencies, array_of_strings: true + validates :extends, type: String end end @@ -81,7 +83,8 @@ module Gitlab :cache, :image, :services, :only, :except, :variables, :artifacts, :commands, :environment, :coverage, :retry - attributes :script, :tags, :allow_failure, :when, :dependencies, :retry + attributes :script, :tags, :allow_failure, :when, :dependencies, + :retry, :extends def compose!(deps = nil) super do diff --git a/lib/gitlab/ci/config/extendable.rb b/lib/gitlab/ci/config/extendable.rb new file mode 100644 index 00000000000..a43901c69fe --- /dev/null +++ b/lib/gitlab/ci/config/extendable.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + class Config + class Extendable + include Enumerable + + ExtensionError = Class.new(StandardError) + + def initialize(hash) + @hash = hash.to_h.deep_dup + + each { |entry| entry.extend! if entry.extensible? } + end + + def each + @hash.each_key do |key| + yield Extendable::Entry.new(key, @hash) + end + end + + def to_hash + @hash.to_h + end + end + end + end +end diff --git a/lib/gitlab/ci/config/extendable/entry.rb b/lib/gitlab/ci/config/extendable/entry.rb new file mode 100644 index 00000000000..7793db09d33 --- /dev/null +++ b/lib/gitlab/ci/config/extendable/entry.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + class Config + class Extendable + class Entry + InvalidExtensionError = Class.new(Extendable::ExtensionError) + CircularDependencyError = Class.new(Extendable::ExtensionError) + NestingTooDeepError = Class.new(Extendable::ExtensionError) + + MAX_NESTING_LEVELS = 10 + + attr_reader :key + + def initialize(key, context, parent = nil) + @key = key + @context = context + @parent = parent + + unless @context.key?(@key) + raise StandardError, 'Invalid entry key!' + end + end + + def extensible? + value.is_a?(Hash) && value.key?(:extends) + end + + def value + @value ||= @context.fetch(@key) + end + + def base_hash! + @base ||= Extendable::Entry + .new(extends_key, @context, self) + .extend! + end + + def extends_key + value.fetch(:extends).to_s.to_sym if extensible? + end + + def ancestors + @ancestors ||= Array(@parent&.ancestors) + Array(@parent&.key) + end + + def extend! + return value unless extensible? + + if unknown_extension? + raise Entry::InvalidExtensionError, + "#{key}: unknown key in `extends`" + end + + if invalid_base? + raise Entry::InvalidExtensionError, + "#{key}: invalid base hash in `extends`" + end + + if nesting_too_deep? + raise Entry::NestingTooDeepError, + "#{key}: nesting too deep in `extends`" + end + + if circular_dependency? + raise Entry::CircularDependencyError, + "#{key}: circular dependency detected in `extends`" + end + + @context[key] = base_hash!.deep_merge(value) + end + + private + + def nesting_too_deep? + ancestors.count > MAX_NESTING_LEVELS + end + + def circular_dependency? + ancestors.include?(key) + end + + def unknown_extension? + !@context.key?(extends_key) + end + + def invalid_base? + !@context[extends_key].is_a?(Hash) + end + end + end + end + end +end diff --git a/lib/gitlab/ci/yaml_processor.rb b/lib/gitlab/ci/yaml_processor.rb index e829f2a95f8..5d1864ae9e2 100644 --- a/lib/gitlab/ci/yaml_processor.rb +++ b/lib/gitlab/ci/yaml_processor.rb @@ -16,7 +16,7 @@ module Gitlab end initial_parsing - rescue Gitlab::Ci::Config::Loader::FormatError => e + rescue Gitlab::Ci::Config::ConfigError => e raise ValidationError, e.message end diff --git a/lib/gitlab/file_detector.rb b/lib/gitlab/file_detector.rb index 49bc9c0b671..8f55e94975c 100644 --- a/lib/gitlab/file_detector.rb +++ b/lib/gitlab/file_detector.rb @@ -8,7 +8,7 @@ module Gitlab # Project files readme: %r{\Areadme[^/]*\z}i, changelog: %r{\A(changelog|history|changes|news)[^/]*\z}i, - license: %r{\A(licen[sc]e|copying)(\.[^/]+)?\z}i, + license: %r{\A((un)?licen[sc]e|copying)(\.[^/]+)?\z}i, contributing: %r{\Acontributing[^/]*\z}i, version: 'version', avatar: /\Alogo\.(png|jpg|gif)\z/, diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb index 258e19a340b..93720500711 100644 --- a/lib/gitlab/git_access.rb +++ b/lib/gitlab/git_access.rb @@ -50,6 +50,10 @@ module Gitlab check_authentication_abilities!(cmd) check_command_disabled!(cmd) check_command_existence!(cmd) + + custom_action = check_custom_action(cmd) + return custom_action if custom_action + check_db_accessibility!(cmd) ensure_project_on_push!(cmd, changes) @@ -65,7 +69,7 @@ module Gitlab check_push_access! end - true + ::Gitlab::GitAccessResult::Success.new end def guest_can_download_code? @@ -92,6 +96,10 @@ module Gitlab private + def check_custom_action(cmd) + nil + end + def check_valid_actor! return unless actor.is_a?(Key) diff --git a/lib/gitlab/git_access_result/custom_action.rb b/lib/gitlab/git_access_result/custom_action.rb new file mode 100644 index 00000000000..a05a4baed82 --- /dev/null +++ b/lib/gitlab/git_access_result/custom_action.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Gitlab + module GitAccessResult + class CustomAction + attr_reader :payload, :message + + # Example of payload: + # + # { + # 'action' => 'geo_proxy_to_primary', + # 'data' => { + # 'api_endpoints' => %w{geo/proxy_git_push_ssh/info_refs geo/proxy_git_push_ssh/push}, + # 'gl_username' => user.username, + # 'primary_repo' => geo_primary_http_url_to_repo(project_or_wiki) + # } + # } + # + def initialize(payload, message) + @payload = payload + @message = message + end + end + end +end diff --git a/lib/gitlab/git_access_result/success.rb b/lib/gitlab/git_access_result/success.rb new file mode 100644 index 00000000000..7bb9f24cb0e --- /dev/null +++ b/lib/gitlab/git_access_result/success.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module Gitlab + module GitAccessResult + class Success + end + end +end diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb index c27972a84a4..12307338972 100644 --- a/lib/gitlab/gitaly_client.rb +++ b/lib/gitlab/gitaly_client.rb @@ -206,7 +206,7 @@ module Gitlab result end - SERVER_FEATURE_FLAGS = %w[gogit_findcommit].freeze + SERVER_FEATURE_FLAGS = %w[gogit_findcommit git_v2].freeze def self.server_feature_flags SERVER_FEATURE_FLAGS.map do |f| diff --git a/lib/gitlab/grape_logging/loggers/route_logger.rb b/lib/gitlab/grape_logging/loggers/route_logger.rb new file mode 100644 index 00000000000..f3146b4dfd9 --- /dev/null +++ b/lib/gitlab/grape_logging/loggers/route_logger.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +# This grape_logging module (https://github.com/aserafin/grape_logging) makes it +# possible to log the details of the action +module Gitlab + module GrapeLogging + module Loggers + class RouteLogger < ::GrapeLogging::Loggers::Base + def parameters(request, _) + endpoint = request.env[Grape::Env::API_ENDPOINT] + route = endpoint&.route&.pattern&.origin + + return {} unless route + + { route: route } + rescue + # endpoint.route calls env[Grape::Env::GRAPE_ROUTING_ARGS][:route_info] + # but env[Grape::Env::GRAPE_ROUTING_ARGS] is nil in the case of a 405 response + # so we're rescuing exceptions and bailing out + {} + end + end + end + end +end diff --git a/lib/gitlab/kubernetes/cluster_role_binding.rb b/lib/gitlab/kubernetes/cluster_role_binding.rb new file mode 100644 index 00000000000..ebea8aff5be --- /dev/null +++ b/lib/gitlab/kubernetes/cluster_role_binding.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module Gitlab + module Kubernetes + class ClusterRoleBinding + attr_reader :name, :cluster_role_name, :subjects + + def initialize(name, cluster_role_name, subjects) + @name = name + @cluster_role_name = cluster_role_name + @subjects = subjects + end + + def generate + ::Kubeclient::Resource.new.tap do |resource| + resource.metadata = metadata + resource.roleRef = role_ref + resource.subjects = subjects + end + end + + private + + def metadata + { name: name } + end + + def role_ref + { + apiGroup: 'rbac.authorization.k8s.io', + kind: 'ClusterRole', + name: cluster_role_name + } + end + end + end +end diff --git a/lib/gitlab/kubernetes/helm.rb b/lib/gitlab/kubernetes/helm.rb index 530ccf88053..4a1bdf34c3e 100644 --- a/lib/gitlab/kubernetes/helm.rb +++ b/lib/gitlab/kubernetes/helm.rb @@ -3,6 +3,9 @@ module Gitlab module Helm HELM_VERSION = '2.7.2'.freeze NAMESPACE = 'gitlab-managed-apps'.freeze + SERVICE_ACCOUNT = 'tiller'.freeze + CLUSTER_ROLE_BINDING = 'tiller-admin'.freeze + CLUSTER_ROLE = 'cluster-admin'.freeze end end end diff --git a/lib/gitlab/kubernetes/helm/api.rb b/lib/gitlab/kubernetes/helm/api.rb index d65374cc23b..2dd74c68075 100644 --- a/lib/gitlab/kubernetes/helm/api.rb +++ b/lib/gitlab/kubernetes/helm/api.rb @@ -9,7 +9,11 @@ module Gitlab def install(command) namespace.ensure_exists! + + create_service_account(command) + create_cluster_role_binding(command) create_config_map(command) + kubeclient.create_pod(command.pod_resource) end @@ -41,6 +45,50 @@ module Gitlab kubeclient.create_config_map(config_map_resource) end end + + def create_service_account(command) + command.service_account_resource.tap do |service_account_resource| + break unless service_account_resource + + if service_account_exists?(service_account_resource) + kubeclient.update_service_account(service_account_resource) + else + kubeclient.create_service_account(service_account_resource) + end + end + end + + def create_cluster_role_binding(command) + command.cluster_role_binding_resource.tap do |cluster_role_binding_resource| + break unless cluster_role_binding_resource + + if cluster_role_binding_exists?(cluster_role_binding_resource) + kubeclient.update_cluster_role_binding(cluster_role_binding_resource) + else + kubeclient.create_cluster_role_binding(cluster_role_binding_resource) + end + end + end + + def service_account_exists?(resource) + resource_exists? do + kubeclient.get_service_account(resource.metadata.name, resource.metadata.namespace) + end + end + + def cluster_role_binding_exists?(resource) + resource_exists? do + kubeclient.get_cluster_role_binding(resource.metadata.name) + end + end + + def resource_exists? + yield + rescue ::Kubeclient::HttpError => e + raise e unless e.error_code == 404 + + false + end end end end diff --git a/lib/gitlab/kubernetes/helm/base_command.rb b/lib/gitlab/kubernetes/helm/base_command.rb index afcfd109de0..6752f2cff43 100644 --- a/lib/gitlab/kubernetes/helm/base_command.rb +++ b/lib/gitlab/kubernetes/helm/base_command.rb @@ -3,7 +3,9 @@ module Gitlab module Helm module BaseCommand def pod_resource - Gitlab::Kubernetes::Helm::Pod.new(self, namespace).generate + pod_service_account_name = rbac? ? service_account_name : nil + + Gitlab::Kubernetes::Helm::Pod.new(self, namespace, service_account_name: pod_service_account_name).generate end def generate_script @@ -26,6 +28,14 @@ module Gitlab Gitlab::Kubernetes::ConfigMap.new(name, files).generate end + def service_account_resource + nil + end + + def cluster_role_binding_resource + nil + end + def file_names files.keys end @@ -34,6 +44,10 @@ module Gitlab raise "Not implemented" end + def rbac? + raise "Not implemented" + end + def files raise "Not implemented" end @@ -47,6 +61,10 @@ module Gitlab def namespace Gitlab::Kubernetes::Helm::NAMESPACE end + + def service_account_name + Gitlab::Kubernetes::Helm::SERVICE_ACCOUNT + end end end end diff --git a/lib/gitlab/kubernetes/helm/init_command.rb b/lib/gitlab/kubernetes/helm/init_command.rb index a4546509515..c7046a9ea75 100644 --- a/lib/gitlab/kubernetes/helm/init_command.rb +++ b/lib/gitlab/kubernetes/helm/init_command.rb @@ -6,9 +6,10 @@ module Gitlab attr_reader :name, :files - def initialize(name:, files:) + def initialize(name:, files:, rbac:) @name = name @files = files + @rbac = rbac end def generate_script @@ -17,15 +18,62 @@ module Gitlab ].join("\n") end + def rbac? + @rbac + end + + def service_account_resource + return unless rbac? + + Gitlab::Kubernetes::ServiceAccount.new(service_account_name, namespace).generate + end + + def cluster_role_binding_resource + return unless rbac? + + subjects = [{ kind: 'ServiceAccount', name: service_account_name, namespace: namespace }] + + Gitlab::Kubernetes::ClusterRoleBinding.new( + cluster_role_binding_name, + cluster_role_name, + subjects + ).generate + end + private def init_helm_command - tls_flags = "--tiller-tls" \ - " --tiller-tls-verify --tls-ca-cert #{files_dir}/ca.pem" \ - " --tiller-tls-cert #{files_dir}/cert.pem" \ - " --tiller-tls-key #{files_dir}/key.pem" + command = %w[helm init] + init_command_flags + + command.shelljoin + " >/dev/null\n" + end + + def init_command_flags + tls_flags + optional_service_account_flag + end + + def tls_flags + [ + '--tiller-tls', + '--tiller-tls-verify', + '--tls-ca-cert', "#{files_dir}/ca.pem", + '--tiller-tls-cert', "#{files_dir}/cert.pem", + '--tiller-tls-key', "#{files_dir}/key.pem" + ] + end + + def optional_service_account_flag + return [] unless rbac? + + ['--service-account', service_account_name] + end + + def cluster_role_binding_name + Gitlab::Kubernetes::Helm::CLUSTER_ROLE_BINDING + end - "helm init #{tls_flags} >/dev/null" + def cluster_role_name + Gitlab::Kubernetes::Helm::CLUSTER_ROLE end end end diff --git a/lib/gitlab/kubernetes/helm/install_command.rb b/lib/gitlab/kubernetes/helm/install_command.rb index 9672f80687e..1be7924d6ac 100644 --- a/lib/gitlab/kubernetes/helm/install_command.rb +++ b/lib/gitlab/kubernetes/helm/install_command.rb @@ -6,10 +6,11 @@ module Gitlab attr_reader :name, :files, :chart, :version, :repository - def initialize(name:, chart:, files:, version: nil, repository: nil) + def initialize(name:, chart:, files:, rbac:, version: nil, repository: nil) @name = name @chart = chart @version = version + @rbac = rbac @files = files @repository = repository end @@ -22,6 +23,10 @@ module Gitlab ].compact.join("\n") end + def rbac? + @rbac + end + private def init_command @@ -29,28 +34,51 @@ module Gitlab end def repository_command - "helm repo add #{name} #{repository}" if repository + ['helm', 'repo', 'add', name, repository].shelljoin if repository end def script_command - init_flags = "--name #{name}#{optional_tls_flags}#{optional_version_flag}" \ - " --namespace #{Gitlab::Kubernetes::Helm::NAMESPACE}" \ - " -f /data/helm/#{name}/config/values.yaml" + command = ['helm', 'install', chart] + install_command_flags + + command.shelljoin + " >/dev/null\n" + end + + def install_command_flags + name_flag = ['--name', name] + namespace_flag = ['--namespace', Gitlab::Kubernetes::Helm::NAMESPACE] + value_flag = ['-f', "/data/helm/#{name}/config/values.yaml"] - "helm install #{chart} #{init_flags} >/dev/null\n" + name_flag + + optional_tls_flags + + optional_version_flag + + optional_rbac_create_flag + + namespace_flag + + value_flag + end + + def optional_rbac_create_flag + return [] unless rbac? + + # jupyterhub helm chart is using rbac.enabled + # https://github.com/jupyterhub/zero-to-jupyterhub-k8s/tree/master/jupyterhub + %w[--set rbac.create=true,rbac.enabled=true] end def optional_version_flag - " --version #{version}" if version + return [] unless version + + ['--version', version] end def optional_tls_flags - return unless files.key?(:'ca.pem') + return [] unless files.key?(:'ca.pem') - " --tls" \ - " --tls-ca-cert #{files_dir}/ca.pem" \ - " --tls-cert #{files_dir}/cert.pem" \ - " --tls-key #{files_dir}/key.pem" + [ + '--tls', + '--tls-ca-cert', "#{files_dir}/ca.pem", + '--tls-cert', "#{files_dir}/cert.pem", + '--tls-key', "#{files_dir}/key.pem" + ] end end end diff --git a/lib/gitlab/kubernetes/helm/pod.rb b/lib/gitlab/kubernetes/helm/pod.rb index 6e5d3388405..95192b11c0d 100644 --- a/lib/gitlab/kubernetes/helm/pod.rb +++ b/lib/gitlab/kubernetes/helm/pod.rb @@ -2,9 +2,10 @@ module Gitlab module Kubernetes module Helm class Pod - def initialize(command, namespace_name) + def initialize(command, namespace_name, service_account_name: nil) @command = command @namespace_name = namespace_name + @service_account_name = service_account_name end def generate @@ -12,13 +13,14 @@ module Gitlab spec[:volumes] = volumes_specification spec[:containers][0][:volumeMounts] = volume_mounts_specification + spec[:serviceAccountName] = service_account_name if service_account_name ::Kubeclient::Resource.new(metadata: metadata, spec: spec) end private - attr_reader :command, :namespace_name, :kubeclient, :config_map + attr_reader :command, :namespace_name, :service_account_name def container_specification { diff --git a/lib/gitlab/kubernetes/kube_client.rb b/lib/gitlab/kubernetes/kube_client.rb new file mode 100644 index 00000000000..8312b901524 --- /dev/null +++ b/lib/gitlab/kubernetes/kube_client.rb @@ -0,0 +1,110 @@ +# frozen_string_literal: true + +require 'uri' + +module Gitlab + module Kubernetes + # Wrapper around Kubeclient::Client to dispatch + # the right message to the client that can respond to the message. + # We must have a kubeclient for each ApiGroup as there is no + # other way to use the Kubeclient gem. + # + # See https://github.com/abonas/kubeclient/issues/348. + class KubeClient + include Gitlab::Utils::StrongMemoize + + SUPPORTED_API_GROUPS = [ + 'api', + 'apis/rbac.authorization.k8s.io', + 'apis/extensions' + ].freeze + + # Core API methods delegates to the core api group client + delegate :get_pods, + :get_secrets, + :get_config_map, + :get_namespace, + :get_pod, + :get_service, + :get_service_account, + :delete_pod, + :create_config_map, + :create_namespace, + :create_pod, + :create_service_account, + :update_config_map, + :update_service_account, + to: :core_client + + # RBAC methods delegates to the apis/rbac.authorization.k8s.io api + # group client + delegate :create_cluster_role_binding, + :get_cluster_role_binding, + :update_cluster_role_binding, + to: :rbac_client + + # Deployments resource is currently on the apis/extensions api group + delegate :get_deployments, + to: :extensions_client + + # non-entity methods that can only work with the core client + # as it uses the pods/log resource + delegate :get_pod_log, + :watch_pod_log, + to: :core_client + + def initialize(api_prefix, api_groups = ['api'], api_version = 'v1', **kubeclient_options) + raise ArgumentError unless check_api_groups_supported?(api_groups) + + @api_prefix = api_prefix + @api_groups = api_groups + @api_version = api_version + @kubeclient_options = kubeclient_options + end + + def discover! + clients.each(&:discover) + end + + def clients + hashed_clients.values + end + + def core_client + hashed_clients['api'] + end + + def rbac_client + hashed_clients['apis/rbac.authorization.k8s.io'] + end + + def extensions_client + hashed_clients['apis/extensions'] + end + + def hashed_clients + strong_memoize(:hashed_clients) do + @api_groups.map do |api_group| + api_url = join_api_url(@api_prefix, api_group) + [api_group, ::Kubeclient::Client.new(api_url, @api_version, **@kubeclient_options)] + end.to_h + end + end + + private + + def check_api_groups_supported?(api_groups) + api_groups.all? {|api_group| SUPPORTED_API_GROUPS.include?(api_group) } + end + + def join_api_url(api_prefix, api_path) + url = URI.parse(api_prefix) + prefix = url.path.sub(%r{/+\z}, '') + + url.path = [prefix, api_path].join("/") + + url.to_s + end + end + end +end diff --git a/lib/gitlab/kubernetes/service_account.rb b/lib/gitlab/kubernetes/service_account.rb new file mode 100644 index 00000000000..d58fc1c3976 --- /dev/null +++ b/lib/gitlab/kubernetes/service_account.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Gitlab + module Kubernetes + class ServiceAccount + attr_reader :name, :namespace_name + + def initialize(name, namespace_name) + @name = name + @namespace_name = namespace_name + end + + def generate + ::Kubeclient::Resource.new(metadata: metadata) + end + + private + + def metadata + { + name: name, + namespace: namespace_name + } + end + end + end +end diff --git a/lib/gitlab/metrics/subscribers/active_record.rb b/lib/gitlab/metrics/subscribers/active_record.rb index c205f348023..04107296ae3 100644 --- a/lib/gitlab/metrics/subscribers/active_record.rb +++ b/lib/gitlab/metrics/subscribers/active_record.rb @@ -6,9 +6,15 @@ module Gitlab include Gitlab::Metrics::Methods attach_to :active_record + IGNORABLE_SQL = %w{BEGIN COMMIT}.freeze + def sql(event) return unless current_transaction + payload = event.payload + + return if payload[:name] == 'SCHEMA' || IGNORABLE_SQL.include?(payload[:sql]) + self.class.gitlab_sql_duration_seconds.observe(current_transaction.labels, event.duration / 1000.0) current_transaction.increment(:sql_duration, event.duration, false) diff --git a/lib/gitlab/middleware/multipart.rb b/lib/gitlab/middleware/multipart.rb index 3d588918adf..10cb1e7127e 100644 --- a/lib/gitlab/middleware/multipart.rb +++ b/lib/gitlab/middleware/multipart.rb @@ -83,7 +83,7 @@ module Gitlab def open_file(params, key) allowed_paths = [ - FileUploader.root, + ::FileUploader.root, Gitlab.config.uploads.storage_path, File.join(Rails.root, 'public/uploads/tmp') ] diff --git a/lib/gitlab/template/base_template.rb b/lib/gitlab/template/base_template.rb index 7393574ac13..3770f3f250b 100644 --- a/lib/gitlab/template/base_template.rb +++ b/lib/gitlab/template/base_template.rb @@ -1,14 +1,18 @@ module Gitlab module Template class BaseTemplate - def initialize(path, project = nil) + attr_reader :category + + def initialize(path, project = nil, category: nil) @path = path + @category = category @finder = self.class.finder(project) end def name File.basename(@path, self.class.extension) end + alias_method :id, :name def content @finder.read(@path) @@ -62,7 +66,7 @@ module Gitlab directory = category_directory(category) files = finder(project).list_files_for(directory) - files.map { |f| new(f, project) }.sort + files.map { |f| new(f, project, category: category) }.sort end def category_directory(category) diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb index 7797bd5fab2..957908f183d 100644 --- a/lib/gitlab/usage_data.rb +++ b/lib/gitlab/usage_data.rb @@ -22,7 +22,7 @@ module Gitlab hostname: Gitlab.config.gitlab.host, version: Gitlab::VERSION, installation_type: Gitlab::INSTALLATION_TYPE, - active_user_count: User.active.count, + active_user_count: count(User.active), recorded_at: Time.now, edition: 'CE' } @@ -34,51 +34,51 @@ module Gitlab def system_usage_data { counts: { - assignee_lists: List.assignee.count, - boards: Board.count, - 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, - clusters: ::Clusters::Cluster.count, - clusters_enabled: ::Clusters::Cluster.enabled.count, - clusters_disabled: ::Clusters::Cluster.disabled.count, - clusters_platforms_gke: ::Clusters::Cluster.gcp_installed.enabled.count, - clusters_platforms_user: ::Clusters::Cluster.user_provided.enabled.count, - clusters_applications_helm: ::Clusters::Applications::Helm.installed.count, - clusters_applications_ingress: ::Clusters::Applications::Ingress.installed.count, - clusters_applications_prometheus: ::Clusters::Applications::Prometheus.installed.count, - clusters_applications_runner: ::Clusters::Applications::Runner.installed.count, - in_review_folder: ::Environment.in_review_folder.count, - groups: Group.count, - issues: Issue.count, - keys: Key.count, - label_lists: List.label.count, - labels: Label.count, - lfs_objects: LfsObject.count, - merge_requests: MergeRequest.count, - milestone_lists: List.milestone.count, - milestones: Milestone.count, - notes: Note.count, - pages_domains: PagesDomain.count, - projects: Project.count, - projects_imported_from_github: Project.where(import_type: 'github').count, - protected_branches: ProtectedBranch.count, - releases: Release.count, - remote_mirrors: RemoteMirror.count, - snippets: Snippet.count, - todos: Todo.count, - uploads: Upload.count, - web_hooks: WebHook.count + assignee_lists: count(List.assignee), + boards: count(Board), + ci_builds: count(::Ci::Build), + ci_internal_pipelines: count(::Ci::Pipeline.internal), + ci_external_pipelines: count(::Ci::Pipeline.external), + ci_pipeline_config_auto_devops: count(::Ci::Pipeline.auto_devops_source), + ci_pipeline_config_repository: count(::Ci::Pipeline.repository_source), + ci_runners: count(::Ci::Runner), + ci_triggers: count(::Ci::Trigger), + ci_pipeline_schedules: count(::Ci::PipelineSchedule), + auto_devops_enabled: count(::ProjectAutoDevops.enabled), + auto_devops_disabled: count(::ProjectAutoDevops.disabled), + deploy_keys: count(DeployKey), + deployments: count(Deployment), + environments: count(::Environment), + clusters: count(::Clusters::Cluster), + clusters_enabled: count(::Clusters::Cluster.enabled), + clusters_disabled: count(::Clusters::Cluster.disabled), + clusters_platforms_gke: count(::Clusters::Cluster.gcp_installed.enabled), + clusters_platforms_user: count(::Clusters::Cluster.user_provided.enabled), + clusters_applications_helm: count(::Clusters::Applications::Helm.installed), + clusters_applications_ingress: count(::Clusters::Applications::Ingress.installed), + clusters_applications_prometheus: count(::Clusters::Applications::Prometheus.installed), + clusters_applications_runner: count(::Clusters::Applications::Runner.installed), + in_review_folder: count(::Environment.in_review_folder), + groups: count(Group), + issues: count(Issue), + keys: count(Key), + label_lists: count(List.label), + labels: count(Label), + lfs_objects: count(LfsObject), + merge_requests: count(MergeRequest), + milestone_lists: count(List.milestone), + milestones: count(Milestone), + notes: count(Note), + pages_domains: count(PagesDomain), + projects: count(Project), + projects_imported_from_github: count(Project.where(import_type: 'github')), + protected_branches: count(ProtectedBranch), + releases: count(Release), + remote_mirrors: count(RemoteMirror), + snippets: count(Snippet), + todos: count(Todo), + uploads: count(Upload), + web_hooks: count(WebHook) }.merge(services_usage) } end @@ -120,8 +120,14 @@ module Gitlab PrometheusService: :projects_prometheus_active } - results = Service.unscoped.where(type: types.keys, active: true).group(:type).count - results.each_with_object({}) { |(key, value), response| response[types[key.to_sym]] = value } + results = count(Service.unscoped.where(type: types.keys, active: true).group(:type), fallback: Hash.new(-1)) + types.each_with_object({}) { |(klass, key), response| response[key] = results[klass.to_s] || 0 } + end + + def count(relation, fallback: -1) + relation.count + rescue ActiveRecord::StatementInvalid + fallback end end end diff --git a/lib/gitlab/user_extractor.rb b/lib/gitlab/user_extractor.rb new file mode 100644 index 00000000000..3ede0a5b5e6 --- /dev/null +++ b/lib/gitlab/user_extractor.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +# This class extracts all users found in a piece of text by the username or the +# email adress + +module Gitlab + class UserExtractor + # Not using `Devise.email_regexp` to filter out any chars that an email + # does not end with and not pinning the email to a start of end of a string. + EMAIL_REGEXP = /(?<email>([^@\s]+@[^@\s]+(?<!\W)))/ + USERNAME_REGEXP = User.reference_pattern + + def initialize(text) + @text = text + end + + def users + return User.none unless @text.present? + + @users ||= User.from("(#{union.to_sql}) users") + end + + def usernames + matches[:usernames] + end + + def emails + matches[:emails] + end + + def references + @references ||= matches.values.flatten + end + + def matches + @matches ||= { + emails: @text.scan(EMAIL_REGEXP).flatten.uniq, + usernames: @text.scan(USERNAME_REGEXP).flatten.uniq + } + end + + private + + def union + relations = [] + + relations << User.by_any_email(emails) if emails.any? + relations << User.by_username(usernames) if usernames.any? + + Gitlab::SQL::Union.new(relations) + end + end +end diff --git a/lib/object_storage/direct_upload.rb b/lib/object_storage/direct_upload.rb index b372b4af090..ab43910c8bd 100644 --- a/lib/object_storage/direct_upload.rb +++ b/lib/object_storage/direct_upload.rb @@ -158,7 +158,7 @@ module ObjectStorage end def upload_options - { 'Content-Type' => 'application/octet-stream' } + {} end def connection diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 7d0bd01142c..00f9c6aa95a 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -150,6 +150,12 @@ msgstr "" msgid "%{unstaged} unstaged and %{staged} staged changes" msgstr "" +msgid "%{usage_ping_link_start}Learn more%{usage_ping_link_end} about what information is shared with GitLab Inc." +msgstr "" + +msgid "+ %{count} more" +msgstr "" + msgid "+ %{moreCount} more" msgstr "" @@ -319,10 +325,10 @@ msgstr "" msgid "Add Kubernetes cluster" msgstr "" -msgid "Add License" +msgid "Add Readme" msgstr "" -msgid "Add Readme" +msgid "Add license" msgstr "" msgid "Add new application" @@ -1356,6 +1362,9 @@ msgstr "" msgid "ClusterIntegration|Did you know?" msgstr "" +msgid "ClusterIntegration|Enable this setting if using role-based access control (RBAC)." +msgstr "" + msgid "ClusterIntegration|Enter the details for your Kubernetes cluster" msgstr "" @@ -1539,6 +1548,9 @@ msgstr "" msgid "ClusterIntegration|Prometheus is an open-source monitoring system with %{gitlabIntegrationLink} to monitor deployed applications." msgstr "" +msgid "ClusterIntegration|RBAC-enabled cluster (experimental)" +msgstr "" + msgid "ClusterIntegration|Read our %{link_to_help_page} on Kubernetes cluster integration." msgstr "" @@ -1617,6 +1629,9 @@ msgstr "" msgid "ClusterIntegration|This account must have permissions to create a Kubernetes cluster in the %{link_to_container_project} specified below" msgstr "" +msgid "ClusterIntegration|This option will allow you to install applications on RBAC clusters." +msgstr "" + msgid "ClusterIntegration|Toggle Kubernetes Cluster" msgstr "" @@ -1897,6 +1912,15 @@ msgstr "" msgid "ConvDev Index" msgstr "" +msgid "Copy %{protocol} clone URL" +msgstr "" + +msgid "Copy HTTPS clone URL" +msgstr "" + +msgid "Copy SSH clone URL" +msgstr "" + msgid "Copy URL to clipboard" msgstr "" @@ -1990,9 +2014,6 @@ msgstr "" msgid "Create project label" msgstr "" -msgid "CreateNewFork|Fork" -msgstr "" - msgid "CreateTag|Tag" msgstr "" @@ -2412,6 +2433,12 @@ msgstr "" msgid "Enable the Performance Bar for a given group." msgstr "" +msgid "Enable usage ping" +msgstr "" + +msgid "Enable usage ping to get an overview of how you are using GitLab from a feature perspective." +msgstr "" + msgid "Ends at (UTC)" msgstr "" @@ -2724,17 +2751,15 @@ msgstr "" msgid "For internal projects, any logged in user can view pipelines and access job details (output logs and artifacts)" msgstr "" +msgid "For more information, see the documentation on %{deactivating_usage_ping_link_start}deactivating the usage ping%{deactivating_usage_ping_link_end}." +msgstr "" + msgid "For private projects, any member (guest or higher) can view pipelines and access job details (output logs and artifacts)" msgstr "" msgid "For public projects, anyone can view pipelines and access job details (output logs and artifacts)" msgstr "" -msgid "Fork" -msgid_plural "Forks" -msgstr[0] "" -msgstr[1] "" - msgid "ForkedFromProjectPath|Forked from" msgstr "" @@ -2858,12 +2883,6 @@ msgstr "" msgid "Go to %{link_to_google_takeout}." msgstr "" -msgid "Go to your fork" -msgstr "" - -msgid "GoToYourFork|Fork" -msgstr "" - msgid "Google Code import" msgstr "" @@ -3035,6 +3054,9 @@ msgstr "" msgid "Help page text and support page url." msgstr "" +msgid "Hide payload" +msgstr "" + msgid "Hide value" msgid_plural "Hide values" msgstr[0] "" @@ -3175,6 +3197,9 @@ msgstr "" msgid "Import repository" msgstr "" +msgid "In order to enable instance-level analytics, please ask an admin to enable %{usage_ping_link_start}usage ping%{usage_ping_link_end}." +msgstr "" + msgid "In the next step, you'll be able to select the projects you want to import." msgstr "" @@ -3895,6 +3920,9 @@ msgstr "" msgid "No labels with such name or description" msgstr "" +msgid "No license. All rights reserved" +msgstr "" + msgid "No merge requests found" msgstr "" @@ -4374,6 +4402,9 @@ msgstr "" msgid "Preview" msgstr "" +msgid "Preview payload" +msgstr "" + msgid "Prioritize" msgstr "" @@ -4554,6 +4585,27 @@ msgstr "" msgid "ProjectLifecycle|Stage" msgstr "" +msgid "ProjectOverview|Fork" +msgstr "" + +msgid "ProjectOverview|Forks" +msgstr "" + +msgid "ProjectOverview|Go to your fork" +msgstr "" + +msgid "ProjectOverview|Star" +msgstr "" + +msgid "ProjectOverview|Unstar" +msgstr "" + +msgid "ProjectOverview|You have reached your project limit" +msgstr "" + +msgid "ProjectOverview|You must sign in to star a project" +msgstr "" + msgid "ProjectPage|Project ID: %{project_id}" msgstr "" @@ -5642,6 +5694,9 @@ msgstr "" msgid "The update action will time out after %{number_of_minutes} minutes. For big repositories, use a clone/push combination." msgstr "" +msgid "The usage ping is disabled, and cannot be configured through this form." +msgstr "" + msgid "The user map is a JSON document mapping the Google Code users that participated on your projects to the way their email addresses and usernames will be imported into GitLab. You can change this by changing the value on the right hand side of <code>:</code>. Be sure to preserve the surrounding double quotes, other punctuation and the email address or username on the left hand side." msgstr "" @@ -6006,12 +6061,18 @@ msgstr "" msgid "To define internal users, first enable new users set to external" msgstr "" +msgid "To enable it and see User Cohorts, visit %{application_settings_link_start}application settings%{application_settings_link_end}." +msgstr "" + msgid "To get started you enter your FogBugz URL and login information below. In the next steps, you'll be able to map users and select the projects you want to import." msgstr "" msgid "To get started, please enter your Gitea Host URL and a %{link_to_personal_token}." msgstr "" +msgid "To help improve GitLab and its user experience, GitLab will periodically collect usage information." +msgstr "" + msgid "To import GitHub repositories, you can use a %{personal_access_token_link}. When you create your Personal Access Token, you will need to select the <code>repo</code> scope, so we can display a list of your public and private repositories which are available to import." msgstr "" @@ -6171,6 +6232,9 @@ msgstr "" msgid "Upvotes" msgstr "" +msgid "Usage ping is not enabled" +msgstr "" + msgid "Usage statistics" msgstr "" @@ -6192,6 +6256,9 @@ msgstr "" msgid "Use your global notification setting" msgstr "" +msgid "User Cohorts are only shown when the %{usage_ping_link_start}usage ping%{usage_ping_link_end} is enabled." +msgstr "" + msgid "User Settings" msgstr "" @@ -6516,9 +6583,6 @@ msgstr "" msgid "You must have maintainer access to force delete a lock" msgstr "" -msgid "You must sign in to star a project" -msgstr "" - msgid "You need permission." msgstr "" diff --git a/package.json b/package.json index 17ff85c9cd0..7e6ddf0fca7 100644 --- a/package.json +++ b/package.json @@ -69,8 +69,8 @@ "jszip-utils": "^0.0.2", "katex": "^0.8.3", "marked": "^0.3.12", - "monaco-editor": "0.13.1", - "monaco-editor-webpack-plugin": "^1.4.0", + "monaco-editor": "^0.14.3", + "monaco-editor-webpack-plugin": "^1.5.2", "mousetrap": "^1.4.6", "pikaday": "^1.6.1", "popper.js": "^1.14.3", @@ -250,6 +250,7 @@ module QA module Component autoload :ClonePanel, 'qa/page/component/clone_panel' autoload :Dropzone, 'qa/page/component/dropzone' + autoload :GroupsFilter, 'qa/page/component/groups_filter' autoload :Select2, 'qa/page/component/select2' end end diff --git a/qa/qa/factory/resource/group.rb b/qa/qa/factory/resource/group.rb index 531fccd2ad8..033fc48c08f 100644 --- a/qa/qa/factory/resource/group.rb +++ b/qa/qa/factory/resource/group.rb @@ -2,7 +2,7 @@ module QA module Factory module Resource class Group < Factory::Base - attr_writer :path, :description + attr_accessor :path, :description dependency Factory::Resource::Sandbox, as: :sandbox @@ -14,17 +14,23 @@ module QA def fabricate! sandbox.visit! - Page::Group::Show.perform do |page| - if page.has_subgroup?(@path) - page.go_to_subgroup(@path) + Page::Group::Show.perform do |group_show| + if group_show.has_subgroup?(path) + group_show.go_to_subgroup(path) else - page.go_to_new_subgroup + group_show.go_to_new_subgroup - Page::Group::New.perform do |group| - group.set_path(@path) - group.set_description(@description) - group.set_visibility('Public') - group.create + Page::Group::New.perform do |group_new| + group_new.set_path(path) + group_new.set_description(description) + group_new.set_visibility('Public') + group_new.create + end + + # Ensure that the group was actually created + group_show.wait(time: 1) do + group_show.has_text?(path) && + group_show.has_new_project_or_subgroup_dropdown? end end end diff --git a/qa/qa/page/component/groups_filter.rb b/qa/qa/page/component/groups_filter.rb new file mode 100644 index 00000000000..69d465e8ac7 --- /dev/null +++ b/qa/qa/page/component/groups_filter.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module QA + module Page + module Component + module GroupsFilter + def self.included(base) + base.view 'app/views/shared/groups/_search_form.html.haml' do + element :groups_filter, 'search_field_tag :filter' + element :groups_filter_placeholder, 'Filter by name...' + end + + base.view 'app/views/shared/groups/_empty_state.html.haml' do + element :groups_empty_state + end + + base.view 'app/assets/javascripts/groups/components/groups.vue' do + element :groups_list_tree_container + end + end + + private + + def filter_by_name(name) + wait(reload: false) do + page.has_css?(element_selector_css(:groups_empty_state)) || + page.has_css?(element_selector_css(:groups_list_tree_container)) + end + + fill_in 'Filter by name...', with: name + end + end + end + end +end diff --git a/qa/qa/page/dashboard/groups.rb b/qa/qa/page/dashboard/groups.rb index e853e0d85e0..5654cc01e09 100644 --- a/qa/qa/page/dashboard/groups.rb +++ b/qa/qa/page/dashboard/groups.rb @@ -2,19 +2,12 @@ module QA module Page module Dashboard class Groups < Page::Base - view 'app/views/shared/groups/_search_form.html.haml' do - element :groups_filter, 'search_field_tag :filter' - element :groups_filter_placeholder, 'Filter by name...' - end + include Page::Component::GroupsFilter view 'app/views/dashboard/_groups_head.html.haml' do element :new_group_button, 'link_to _("New group")' end - def filter_by_name(name) - fill_in 'Filter by name...', with: name - end - def has_group?(name) filter_by_name(name) diff --git a/qa/qa/page/dashboard/projects.rb b/qa/qa/page/dashboard/projects.rb index 73942cb856a..5b2827c089c 100644 --- a/qa/qa/page/dashboard/projects.rb +++ b/qa/qa/page/dashboard/projects.rb @@ -3,6 +3,7 @@ module QA module Dashboard class Projects < Page::Base view 'app/views/dashboard/projects/index.html.haml' + view 'app/views/shared/projects/_search_form.html.haml' do element :form_filter_by_name, /form_tag.+id: 'project-filter-form'/ end @@ -13,6 +14,8 @@ module QA find_link(text: name).click end + private + def filter_by_name(name) page.within('form#project-filter-form') do fill_in :name, with: name diff --git a/qa/qa/page/group/show.rb b/qa/qa/page/group/show.rb index 3e0eaa392f5..ac85f16d8af 100644 --- a/qa/qa/page/group/show.rb +++ b/qa/qa/page/group/show.rb @@ -2,6 +2,8 @@ module QA module Page module Group class Show < Page::Base + include Page::Component::GroupsFilter + view 'app/views/groups/show.html.haml' do element :new_project_or_subgroup_dropdown, '.new-project-subgroup' element :new_project_or_subgroup_dropdown_toggle, '.dropdown-toggle' @@ -21,8 +23,8 @@ module QA click_link name end - def filter_by_name(name) - fill_in 'Filter by name...', with: name + def has_new_project_or_subgroup_dropdown? + page.has_css?(element_selector_css(:new_project_or_subgroup_dropdown)) end def has_subgroup?(name) diff --git a/qa/qa/page/project/show.rb b/qa/qa/page/project/show.rb index 07b4d0b745d..267e7bbc249 100644 --- a/qa/qa/page/project/show.rb +++ b/qa/qa/page/project/show.rb @@ -23,7 +23,7 @@ module QA end view 'app/views/projects/buttons/_fork.html.haml' do - element :fork_label, "%span= s_('GoToYourFork|Fork')" + element :fork_label, "%span= s_('ProjectOverview|Fork')" element :fork_link, "link_to new_project_fork_path(@project)" end @@ -32,7 +32,7 @@ module QA end view 'app/presenters/project_presenter.rb' do - element :new_file_button, "label: _('New file')," + element :new_file_button, "_('New file')," end def project_name diff --git a/rubocop/cop/line_break_around_conditional_block.rb b/rubocop/cop/line_break_around_conditional_block.rb index 59fe6e5d98c..8118b314b63 100644 --- a/rubocop/cop/line_break_around_conditional_block.rb +++ b/rubocop/cop/line_break_around_conditional_block.rb @@ -77,7 +77,8 @@ module RuboCop start_clause_line?(previous_line(node)) || block_start?(previous_line(node)) || begin_line?(previous_line(node)) || - assignment_line?(previous_line(node)) + assignment_line?(previous_line(node)) || + rescue_line?(previous_line(node)) end def last_line_valid?(node) @@ -111,6 +112,10 @@ module RuboCop line =~ /^\s*.*=/ end + def rescue_line?(line) + line =~ /^\s*rescue/ + end + def block_start?(line) line.match(/ (do|{)( \|.*?\|)?\s?$/) end diff --git a/spec/controllers/concerns/send_file_upload_spec.rb b/spec/controllers/concerns/send_file_upload_spec.rb index 58bb91a0c80..767fba7fd58 100644 --- a/spec/controllers/concerns/send_file_upload_spec.rb +++ b/spec/controllers/concerns/send_file_upload_spec.rb @@ -52,7 +52,7 @@ describe SendFileUpload do end context 'with attachment' do - subject { controller.send_upload(uploader, attachment: 'test.js') } + let(:send_attachment) { controller.send_upload(uploader, attachment: 'test.js') } it 'sends a file with content-type of text/plain' do expected_params = { @@ -62,7 +62,29 @@ describe SendFileUpload do } expect(controller).to receive(:send_file).with(uploader.path, expected_params) - subject + send_attachment + end + + context 'with a proxied file in object storage' do + before do + stub_uploads_object_storage(uploader: uploader_class) + uploader.object_store = ObjectStorage::Store::REMOTE + uploader.store!(temp_file) + allow(Gitlab.config.uploads.object_store).to receive(:proxy_download) { true } + end + + it 'sends a file with a custom type' do + headers = double + expected_headers = %r(response-content-disposition=attachment%3Bfilename%3D%22test.js%22&response-content-type=application/javascript) + expect(Gitlab::Workhorse).to receive(:send_url).with(expected_headers).and_call_original + expect(headers).to receive(:store).with(Gitlab::Workhorse::SEND_DATA_HEADER, /^send-url:/) + + expect(controller).not_to receive(:send_file) + expect(controller).to receive(:headers) { headers } + expect(controller).to receive(:head).with(:ok) + + send_attachment + end end end @@ -80,7 +102,12 @@ describe SendFileUpload do it 'sends a file' do headers = double + expect(Gitlab::Workhorse).not_to receive(:send_url).with(/response-content-disposition/) + expect(Gitlab::Workhorse).not_to receive(:send_url).with(/response-content-type/) + expect(Gitlab::Workhorse).to receive(:send_url).and_call_original + expect(headers).to receive(:store).with(Gitlab::Workhorse::SEND_DATA_HEADER, /^send-url:/) + expect(controller).not_to receive(:send_file) expect(controller).to receive(:headers) { headers } expect(controller).to receive(:head).with(:ok) diff --git a/spec/controllers/instance_statistics/cohorts_controller_spec.rb b/spec/controllers/instance_statistics/cohorts_controller_spec.rb index e4eedede93a..596d3c7abe5 100644 --- a/spec/controllers/instance_statistics/cohorts_controller_spec.rb +++ b/spec/controllers/instance_statistics/cohorts_controller_spec.rb @@ -3,5 +3,19 @@ require 'spec_helper' describe InstanceStatistics::CohortsController do + let(:user) { create(:user) } + + before do + sign_in(user) + end + it_behaves_like 'instance statistics availability' + + it 'renders a 404 when the usage ping is disabled' do + stub_application_setting(usage_ping_enabled: false) + + get :index + + expect(response).to have_gitlab_http_status(:not_found) + end end diff --git a/spec/controllers/projects/clusters_controller_spec.rb b/spec/controllers/projects/clusters_controller_spec.rb index 42917d0d505..26a532ee01d 100644 --- a/spec/controllers/projects/clusters_controller_spec.rb +++ b/spec/controllers/projects/clusters_controller_spec.rb @@ -274,11 +274,43 @@ describe Projects::ClustersController do context 'when creates a cluster' do it 'creates a new cluster' do expect(ClusterProvisionWorker).to receive(:perform_async) + expect { go }.to change { Clusters::Cluster.count } .and change { Clusters::Platforms::Kubernetes.count } + expect(response).to redirect_to(project_cluster_path(project, project.clusters.first)) + + expect(project.clusters.first).to be_user + expect(project.clusters.first).to be_kubernetes + end + end + + context 'when creates a RBAC-enabled cluster' do + let(:params) do + { + cluster: { + name: 'new-cluster', + platform_kubernetes_attributes: { + api_url: 'http://my-url', + token: 'test', + namespace: 'aaa', + authorization_type: 'rbac' + } + } + } + end + + it 'creates a new cluster' do + expect(ClusterProvisionWorker).to receive(:perform_async) + + expect { go }.to change { Clusters::Cluster.count } + .and change { Clusters::Platforms::Kubernetes.count } + + expect(response).to redirect_to(project_cluster_path(project, project.clusters.first)) + expect(project.clusters.first).to be_user expect(project.clusters.first).to be_kubernetes + expect(project.clusters.first).to be_platform_kubernetes_rbac end end end diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb index d9499d7e207..ca7d30fec83 100644 --- a/spec/controllers/projects/jobs_controller_spec.rb +++ b/spec/controllers/projects/jobs_controller_spec.rb @@ -135,7 +135,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do end end - context 'when requesting JSON with failed job' do + context 'when requesting JSON' do let(:merge_request) { create(:merge_request, source_project: project) } before do @@ -147,61 +147,51 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do get_show(id: job.id, format: :json) end - it 'exposes needed information' do - expect(response).to have_gitlab_http_status(:ok) - expect(response).to match_response_schema('job/job_details') - expect(json_response['raw_path']).to match(%r{jobs/\d+/raw\z}) - expect(json_response['merge_request']['path']).to match(%r{merge_requests/\d+\z}) - expect(json_response['new_issue_path']).to include('/issues/new') + context 'when job failed' do + it 'exposes needed information' do + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('job/job_details') + expect(json_response['raw_path']).to match(%r{jobs/\d+/raw\z}) + expect(json_response.dig('merge_request', 'path')).to match(%r{merge_requests/\d+\z}) + expect(json_response['new_issue_path']).to include('/issues/new') + end end - end - - context 'when request JSON for successful job' do - let(:merge_request) { create(:merge_request, source_project: project) } - let(:job) { create(:ci_build, :success, :artifacts, pipeline: pipeline) } - - before do - project.add_developer(user) - sign_in(user) - allow_any_instance_of(Ci::Build).to receive(:merge_request).and_return(merge_request) + context 'when job has artifacts' do + context 'with not expiry date' do + let(:job) { create(:ci_build, :success, :artifacts, pipeline: pipeline) } - get_show(id: job.id, format: :json) - end + it 'exposes needed information' do + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('job/job_details') + expect(json_response['artifact']['download_path']).to match(%r{artifacts/download}) + expect(json_response['artifact']['browse_path']).to match(%r{artifacts/browse}) + expect(json_response['artifact']).not_to have_key(:expired) + expect(json_response['artifact']).not_to have_key(:expired_at) + end + end - it 'exposes needed information' do - expect(response).to have_gitlab_http_status(:ok) - expect(response).to match_response_schema('job/job_details') - expect(json_response['artifact']['download_path']).to match(%r{artifacts/download}) - expect(json_response['artifact']['browse_path']).to match(%r{artifacts/browse}) - expect(json_response['artifact']).not_to have_key(:expired) - expect(json_response['artifact']).not_to have_key(:expired_at) - expect(json_response['raw_path']).to match(%r{jobs/\d+/raw\z}) - expect(json_response.dig('merge_request', 'path')).to match(%r{merge_requests/\d+\z}) + context 'with expiry date' do + let(:job) { create(:ci_build, :success, :artifacts, :expired, pipeline: pipeline) } + + it 'exposes needed information' do + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('job/job_details') + expect(json_response['artifact']).not_to have_key(:download_path) + expect(json_response['artifact']).not_to have_key(:browse_path) + expect(json_response['artifact']['expired']).to eq(true) + expect(json_response['artifact']['expire_at']).not_to be_empty + end + end end - context 'when request JSON for successful job with expired artifacts' do - let(:merge_request) { create(:merge_request, source_project: project) } - let(:job) { create(:ci_build, :success, :artifacts, :expired, pipeline: pipeline) } - - before do - project.add_developer(user) - sign_in(user) - - allow_any_instance_of(Ci::Build).to receive(:merge_request).and_return(merge_request) - - get_show(id: job.id, format: :json) - end + context 'when job has terminal' do + let(:job) { create(:ci_build, :running, :with_runner_session, pipeline: pipeline) } - it 'exposes needed information' do + it 'exposes the terminal path' do expect(response).to have_gitlab_http_status(:ok) expect(response).to match_response_schema('job/job_details') - expect(json_response['artifact']).not_to have_key(:download_path) - expect(json_response['artifact']).not_to have_key(:browse_path) - expect(json_response['artifact']['expired']).to eq(true) - expect(json_response['artifact']['expire_at']).not_to be_empty - expect(json_response['raw_path']).to match(%r{jobs/\d+/raw\z}) - expect(json_response.dig('merge_request', 'path')).to match(%r{merge_requests/\d+\z}) + expect(json_response['terminal_path']).to match(%r{/terminal}) end end end diff --git a/spec/factories/clusters/platforms/kubernetes.rb b/spec/factories/clusters/platforms/kubernetes.rb index 89f6ddebf6a..36ac2372204 100644 --- a/spec/factories/clusters/platforms/kubernetes.rb +++ b/spec/factories/clusters/platforms/kubernetes.rb @@ -16,5 +16,9 @@ FactoryBot.define do platform_kubernetes.ca_cert = File.read(pem_file) end end + + trait :rbac_enabled do + authorization_type :rbac + end end end diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index 1215b04913e..17e457c04a5 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -130,6 +130,33 @@ FactoryBot.define do end end + # Build a custom repository by specifying a hash of `filename => content` in + # the transient `files` attribute. Each file will be created in its own + # commit, operating against the master branch. So, the following call: + # + # create(:project, :custom_repo, files: { 'foo/a.txt' => 'foo', 'b.txt' => bar' }) + # + # will create a repository containing two files, and two commits, in master + trait :custom_repo do + transient do + files {} + end + + after :create do |project, evaluator| + raise "Failed to create repository!" unless project.create_repository + + evaluator.files.each do |filename, content| + project.repository.create_file( + project.creator, + filename, + content, + message: "Automatically created file #{filename}", + branch_name: 'master' + ) + end + end + end + # Test repository - https://gitlab.com/gitlab-org/gitlab-test trait :repository do test_repo diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb index a3229fe1741..3c65b5898b4 100644 --- a/spec/features/admin/admin_settings_spec.rb +++ b/spec/features/admin/admin_settings_spec.rb @@ -336,6 +336,15 @@ describe 'Admin updates settings' do expect(find_field('ED25519 SSH keys').value).to eq(forbidden) end + it 'loads usage ping payload on click', :js do + expect(page).to have_button 'Preview payload' + + find('.js-usage-ping-payload-trigger').click + + expect(page).to have_selector '.js-usage-ping-payload' + expect(page).to have_button 'Hide payload' + end + def check_all_events page.check('Active') page.check('Push') diff --git a/spec/features/instance_statistics/cohorts_spec.rb b/spec/features/instance_statistics/cohorts_spec.rb index 573f8600be1..81fc5eff980 100644 --- a/spec/features/instance_statistics/cohorts_spec.rb +++ b/spec/features/instance_statistics/cohorts_spec.rb @@ -12,12 +12,4 @@ describe 'Cohorts page' do expect(page).to have_content("#{Time.now.strftime('%b %Y')} 3 0") end - - it 'shows usage data', :js do - visit instance_statistics_cohorts_path - - wait_for_requests - - expect(find('.js-syntax-highlight').text).not_to eq('') - end end diff --git a/spec/features/instance_statistics/conversational_development_index_spec.rb b/spec/features/instance_statistics/conversational_development_index_spec.rb index a6c16b6a2a3..d8be554d734 100644 --- a/spec/features/instance_statistics/conversational_development_index_spec.rb +++ b/spec/features/instance_statistics/conversational_development_index_spec.rb @@ -16,13 +16,21 @@ describe 'Conversational Development Index' do end context 'when usage ping is disabled' do - it 'shows empty state' do + before do stub_application_setting(usage_ping_enabled: false) + end + it 'shows empty state' do visit instance_statistics_conversational_development_index_index_path expect(page).to have_content('Usage ping is not enabled') end + + it 'hides the intro callout' do + visit instance_statistics_conversational_development_index_index_path + + expect(page).not_to have_content 'Introducing Your Conversational Development Index' + end end context 'when there is no data to display' do diff --git a/spec/features/instance_statistics/instance_statistics.rb b/spec/features/instance_statistics/instance_statistics.rb new file mode 100644 index 00000000000..d03e6e68075 --- /dev/null +++ b/spec/features/instance_statistics/instance_statistics.rb @@ -0,0 +1,23 @@ +require 'rails_helper' + +describe 'Cohorts page', :js do + before do + sign_in(create(:admin)) + end + + it 'hides cohorts nav button when usage ping is disabled' do + stub_application_setting(usage_ping_enabled: false) + + visit instance_statistics_root_path + + expect(find('.nav-sidebar')).not_to have_content('Cohorts') + end + + it 'shows cohorts nav button when usage ping is enabled' do + stub_application_setting(usage_ping_enabled: true) + + visit instance_statistics_root_path + + expect(find('.nav-sidebar')).to have_content('Cohorts') + end +end diff --git a/spec/features/projects/blobs/blob_show_spec.rb b/spec/features/projects/blobs/blob_show_spec.rb index 1064f72c271..2ba4d4918ff 100644 --- a/spec/features/projects/blobs/blob_show_spec.rb +++ b/spec/features/projects/blobs/blob_show_spec.rb @@ -5,8 +5,8 @@ describe 'File blob', :js do let(:project) { create(:project, :public, :repository) } - def visit_blob(path, anchor: nil, ref: 'master') - visit project_blob_path(project, File.join(ref, path), anchor: anchor) + def visit_blob(path, anchor: nil, ref: 'master', legacy_render: nil) + visit project_blob_path(project, File.join(ref, path), anchor: anchor, legacy_render: legacy_render) wait_for_requests end @@ -142,6 +142,52 @@ describe 'File blob', :js do end end + context 'Markdown rendering' do + before do + project.add_maintainer(project.creator) + + Files::CreateService.new( + project, + project.creator, + start_branch: 'master', + branch_name: 'master', + commit_message: "Add RedCarpet and CommonMark Markdown ", + file_path: 'files/commonmark/file.md', + file_content: "1. one\n - sublist\n" + ).execute + end + + context 'when rendering default markdown' do + before do + visit_blob('files/commonmark/file.md') + + wait_for_requests + end + + it 'renders using CommonMark' do + aggregate_failures do + expect(page).to have_content("sublist") + expect(page).not_to have_xpath("//ol//li//ul") + end + end + end + + context 'when rendering legacy markdown' do + before do + visit_blob('files/commonmark/file.md', legacy_render: 1) + + wait_for_requests + end + + it 'renders using RedCarpet' do + aggregate_failures do + expect(page).to have_content("sublist") + expect(page).to have_xpath("//ol//li//ul") + end + end + end + end + context 'Markdown file (stored in LFS)' do before do project.add_maintainer(project.creator) diff --git a/spec/features/projects/blobs/edit_spec.rb b/spec/features/projects/blobs/edit_spec.rb index 0e036b4ea68..d5b20605860 100644 --- a/spec/features/projects/blobs/edit_spec.rb +++ b/spec/features/projects/blobs/edit_spec.rb @@ -7,6 +7,7 @@ describe 'Editing file blob', :js do let(:merge_request) { create(:merge_request, source_project: project, source_branch: 'feature', target_branch: 'master') } let(:branch) { 'master' } let(:file_path) { project.repository.ls_files(project.repository.root_ref)[1] } + let(:readme_file_path) { 'README.md' } context 'as a developer' do let(:user) { create(:user) } @@ -20,14 +21,19 @@ describe 'Editing file blob', :js do def edit_and_commit(commit_changes: true) wait_for_requests find('.js-edit-blob').click - find('#editor') - execute_script('ace.edit("editor").setValue("class NextFeature\nend\n")') + fill_editor(content: "class NextFeature\\nend\\n") if commit_changes click_button 'Commit changes' end end + def fill_editor(content: "class NextFeature\\nend\\n") + wait_for_requests + find('#editor') + execute_script("ace.edit('editor').setValue('#{content}')") + end + context 'from MR diff' do before do visit diffs_project_merge_request_path(project, merge_request) @@ -63,6 +69,30 @@ describe 'Editing file blob', :js do expect(new_line_count).to be > 0 end end + + context 'when rendering the preview' do + it 'renders content with CommonMark' do + visit project_edit_blob_path(project, tree_join(branch, readme_file_path)) + fill_editor(content: "1. one\\n - sublist\\n") + click_link 'Preview' + wait_for_requests + + # the above generates two seperate lists (not embedded) in CommonMark + expect(page).to have_content("sublist") + expect(page).not_to have_xpath("//ol//li//ul") + end + + it 'renders content with RedCarpet when legacy_render is set' do + visit project_edit_blob_path(project, tree_join(branch, readme_file_path), legacy_render: 1) + fill_editor(content: "1. one\\n - sublist\\n") + click_link 'Preview' + wait_for_requests + + # the above generates a sublist list in RedCarpet + expect(page).to have_content("sublist") + expect(page).to have_xpath("//ol//li//ul") + end + end end context 'visit blob edit' do diff --git a/spec/features/projects/clusters/user_spec.rb b/spec/features/projects/clusters/user_spec.rb index babf47cc341..ec968bfcf7d 100644 --- a/spec/features/projects/clusters/user_spec.rb +++ b/spec/features/projects/clusters/user_spec.rb @@ -38,6 +38,28 @@ describe 'User Cluster', :js do end end + context 'rbac_clusters feature flag is enabled' do + before do + stub_feature_flags(rbac_clusters: true) + + fill_in 'cluster_name', with: 'dev-cluster' + fill_in 'cluster_platform_kubernetes_attributes_api_url', with: 'http://example.com' + fill_in 'cluster_platform_kubernetes_attributes_token', with: 'my-token' + check 'cluster_platform_kubernetes_attributes_authorization_type' + click_button 'Add Kubernetes cluster' + end + + it 'user sees a cluster details page' do + expect(page).to have_content('Kubernetes cluster integration') + expect(page.find_field('cluster[name]').value).to eq('dev-cluster') + expect(page.find_field('cluster[platform_kubernetes_attributes][api_url]').value) + .to have_content('http://example.com') + expect(page.find_field('cluster[platform_kubernetes_attributes][token]').value) + .to have_content('my-token') + expect(page.find_field('cluster[platform_kubernetes_attributes][authorization_type]', disabled: true)).to be_checked + end + end + context 'when user filled form with invalid parameters' do before do click_button 'Add Kubernetes cluster' diff --git a/spec/features/projects/files/project_owner_creates_license_file_spec.rb b/spec/features/projects/files/project_owner_creates_license_file_spec.rb index ac6c8c337fa..6762460971f 100644 --- a/spec/features/projects/files/project_owner_creates_license_file_spec.rb +++ b/spec/features/projects/files/project_owner_creates_license_file_spec.rb @@ -36,7 +36,7 @@ describe 'Projects > Files > Project owner creates a license file', :js do end it 'project maintainer creates a license file from the "Add license" link' do - click_link 'Add License' + click_link 'Add license' expect(page).to have_content('New file') expect(current_path).to eq( diff --git a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb index 801291c1f77..0b8474fb87a 100644 --- a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb +++ b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb @@ -10,7 +10,7 @@ describe 'Projects > Files > Project owner sees a link to create a license file it 'project maintainer creates a license file from a template' do visit project_path(project) - click_on 'Add License' + click_on 'Add license' expect(page).to have_content('New file') expect(current_path).to eq( diff --git a/spec/features/projects/settings/integration_settings_spec.rb b/spec/features/projects/settings/integration_settings_spec.rb index 8745ff72df0..32959969f54 100644 --- a/spec/features/projects/settings/integration_settings_spec.rb +++ b/spec/features/projects/settings/integration_settings_spec.rb @@ -51,6 +51,7 @@ describe 'Projects > Settings > Integration settings' do fill_in 'hook_url', with: url check 'Tag push events' + fill_in 'hook_push_events_branch_filter', with: 'master' check 'Enable SSL verification' check 'Job events' diff --git a/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb b/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb index 0405e21a0d7..b8326edd4fd 100644 --- a/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb +++ b/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe 'Projects > Show > User sees setup shortcut buttons' do - # For "New file", "Add License" functionality, + # For "New file", "Add license" functionality, # see spec/features/projects/files/project_owner_creates_license_file_spec.rb # see spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb @@ -58,9 +58,9 @@ describe 'Projects > Show > User sees setup shortcut buttons' do end end - it '"Add License" button linked to new file populated for a license' do - page.within('.project-stats') do - expect(page).to have_link('Add License', href: presenter.add_license_path) + it '"Add license" button linked to new file populated for a license' do + page.within('.project-metadata') do + expect(page).to have_link('Add license', href: presenter.add_license_path) end end @@ -201,13 +201,13 @@ describe 'Projects > Show > User sees setup shortcut buttons' do end end - it 'no "Add License" button if the project already has a license' do + it 'no "Add license" button if the project already has a license' do visit project_path(project) expect(project.repository.license_blob).not_to be_nil page.within('.project-stats') do - expect(page).not_to have_link('Add License') + expect(page).not_to have_link('Add license') end end diff --git a/spec/features/projects/wiki/markdown_preview_spec.rb b/spec/features/projects/wiki/markdown_preview_spec.rb index ed5f8105487..f505023d1d0 100644 --- a/spec/features/projects/wiki/markdown_preview_spec.rb +++ b/spec/features/projects/wiki/markdown_preview_spec.rb @@ -162,6 +162,34 @@ describe 'Projects > Wiki > User previews markdown changes', :js do expect(page.html).to include("<a href=\"/#{project.full_path}/wikis/title%20with%20spaces\">spaced link</a>") end end + + context 'when rendering the preview' do + it 'renders content with CommonMark' do + create_wiki_page 'a-page/b-page/c-page/common-mark' + click_link 'Edit' + + fill_in :wiki_content, with: "1. one\n - sublist\n" + click_on "Preview" + + # the above generates two seperate lists (not embedded) in CommonMark + expect(page).to have_content("sublist") + expect(page).not_to have_xpath("//ol//li//ul") + end + + it 'renders content with RedCarpet when legacy_render is set' do + wiki_page = create(:wiki_page, + wiki: project.wiki, + attrs: { title: 'home', content: "Empty content" }) + visit(project_wiki_edit_path(project, wiki_page, legacy_render: 1)) + + fill_in :wiki_content, with: "1. one\n - sublist\n" + click_on "Preview" + + # the above generates a sublist list in RedCarpet + expect(page).to have_content("sublist") + expect(page).to have_xpath("//ol//li//ul") + end + end end it "does not linkify double brackets inside code blocks as expected" do diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb index 56ed0c936a6..22e3a99072f 100644 --- a/spec/features/projects_spec.rb +++ b/spec/features/projects_spec.rb @@ -2,6 +2,7 @@ require 'spec_helper' describe 'Project' do include ProjectForksHelper + include MobileHelpers describe 'creating from template' do let(:user) { create(:user) } @@ -54,25 +55,72 @@ describe 'Project' do it 'parses Markdown' do project.update_attribute(:description, 'This is **my** project') visit path - expect(page).to have_css('.project-home-desc > p > strong') + expect(page).to have_css('.project-description > .project-description-markdown > p > strong') end it 'passes through html-pipeline' do project.update_attribute(:description, 'This project is the :poop:') visit path - expect(page).to have_css('.project-home-desc > p > gl-emoji') + expect(page).to have_css('.project-description > .project-description-markdown > p > gl-emoji') end it 'sanitizes unwanted tags' do project.update_attribute(:description, "```\ncode\n```") visit path - expect(page).not_to have_css('.project-home-desc code') + expect(page).not_to have_css('.project-description code') end it 'permits `rel` attribute on links' do project.update_attribute(:description, 'https://google.com/') visit path - expect(page).to have_css('.project-home-desc a[rel]') + expect(page).to have_css('.project-description a[rel]') + end + + context 'read more', :js do + let(:read_more_selector) { '.read-more-container' } + let(:read_more_trigger_selector) { '.project-home-desc .js-read-more-trigger' } + + it 'does not display "read more" link on desktop breakpoint' do + project.update_attribute(:description, 'This is **my** project') + visit path + + expect(find(read_more_trigger_selector, visible: false)).not_to be_visible + end + + it 'displays "read more" link on mobile breakpoint' do + project.update_attribute(:description, 'This is **my** project') + visit path + resize_screen_xs + + find(read_more_trigger_selector).click + + expect(page).to have_css('.project-description .is-expanded') + end + end + end + + describe 'copy clone URL to clipboard', :js do + let(:project) { create(:project, :repository) } + let(:path) { project_path(project) } + + before do + sign_in(create(:admin)) + visit path + end + + context 'desktop component' do + it 'shows on md and larger breakpoints' do + expect(find('.git-clone-holder')).to be_visible + expect(find('.mobile-git-clone', visible: false)).not_to be_visible + end + end + + context 'mobile component' do + it 'shows mobile component on sm and smaller breakpoints' do + resize_screen_xs + expect(find('.mobile-git-clone')).to be_visible + expect(find('.git-clone-holder', visible: false)).not_to be_visible + end end end diff --git a/spec/features/snippets/show_spec.rb b/spec/features/snippets/show_spec.rb index 3fe0b60b18f..367a479f62a 100644 --- a/spec/features/snippets/show_spec.rb +++ b/spec/features/snippets/show_spec.rb @@ -68,23 +68,45 @@ describe 'Snippet', :js do end end - context 'with cached Redcarpet html' do - let(:snippet) { create(:personal_snippet, :public, file_name: file_name, content: content, cached_markdown_version: CacheMarkdownField::CACHE_REDCARPET_VERSION) } + context 'Markdown rendering' do + let(:snippet) { create(:personal_snippet, :public, file_name: file_name, content: content) } let(:file_name) { 'test.md' } let(:content) { "1. one\n - sublist\n" } - it 'renders correctly' do - expect(page).to have_xpath("//ol//li//ul") + context 'when rendering default markdown' do + it 'renders using CommonMark' do + expect(page).to have_content("sublist") + expect(page).not_to have_xpath("//ol//li//ul") + end end - end - context 'with cached CommonMark html' do - let(:snippet) { create(:personal_snippet, :public, file_name: file_name, content: content, cached_markdown_version: CacheMarkdownField::CACHE_COMMONMARK_VERSION) } - let(:file_name) { 'test.md' } - let(:content) { "1. one\n - sublist\n" } + context 'when rendering legacy markdown' do + before do + visit snippet_path(snippet, legacy_render: 1) - it 'renders correctly' do - expect(page).not_to have_xpath("//ol//li//ul") + wait_for_requests + end + + it 'renders using RedCarpet' do + expect(page).to have_content("sublist") + expect(page).to have_xpath("//ol//li//ul") + end + end + + context 'with cached CommonMark html' do + let(:snippet) { create(:personal_snippet, :public, file_name: file_name, content: content, cached_markdown_version: CacheMarkdownField::CACHE_COMMONMARK_VERSION) } + + it 'renders correctly' do + expect(page).not_to have_xpath("//ol//li//ul") + end + end + + context 'with cached Redcarpet html' do + let(:snippet) { create(:personal_snippet, :public, file_name: file_name, content: content, cached_markdown_version: CacheMarkdownField::CACHE_REDCARPET_VERSION) } + + it 'renders correctly' do + expect(page).to have_xpath("//ol//li//ul") + end end end diff --git a/spec/finders/template_finder_spec.rb b/spec/finders/template_finder_spec.rb new file mode 100644 index 00000000000..1d399e8194f --- /dev/null +++ b/spec/finders/template_finder_spec.rb @@ -0,0 +1,48 @@ +require 'spec_helper' + +describe TemplateFinder do + using RSpec::Parameterized::TableSyntax + + describe '#build' do + where(:type, :expected_class) do + :dockerfiles | described_class + :gitignores | described_class + :gitlab_ci_ymls | described_class + :licenses | ::LicenseTemplateFinder + end + + with_them do + subject { described_class.build(type) } + + it { is_expected.to be_a(expected_class) } + end + end + + describe '#execute' do + where(:type, :vendored_name) do + :dockerfiles | 'Binary' + :gitignores | 'Actionscript' + :gitlab_ci_ymls | 'Android' + end + + with_them do + it 'returns all vendored templates when no name is specified' do + result = described_class.new(type).execute + + expect(result).to include(have_attributes(name: vendored_name)) + end + + it 'returns only the specified vendored template when a name is specified' do + result = described_class.new(type, name: vendored_name).execute + + expect(result).to have_attributes(name: vendored_name) + end + + it 'returns nil when an unknown name is specified' do + result = described_class.new(type, name: 'unknown').execute + + expect(result).to be_nil + end + end + end +end diff --git a/spec/fixtures/api/schemas/job/job_details.json b/spec/fixtures/api/schemas/job/job_details.json index 73eca83d788..b8c099250be 100644 --- a/spec/fixtures/api/schemas/job/job_details.json +++ b/spec/fixtures/api/schemas/job/job_details.json @@ -2,6 +2,7 @@ "allOf": [{ "$ref": "job.json" }], "description": "An extension of job.json with more detailed information", "properties": { - "artifact": { "$ref": "artifact.json" } + "artifact": { "$ref": "artifact.json" }, + "terminal_path": { "type": "string" } } } diff --git a/spec/helpers/markup_helper_spec.rb b/spec/helpers/markup_helper_spec.rb index 597648b064d..9a29ac26eff 100644 --- a/spec/helpers/markup_helper_spec.rb +++ b/spec/helpers/markup_helper_spec.rb @@ -25,17 +25,17 @@ describe MarkupHelper do let(:actual) { "#{merge_request.to_reference} -> #{commit.to_reference} -> #{issue.to_reference}" } it "links to the merge request" do - expected = project_merge_request_path(project, merge_request) + expected = urls.project_merge_request_path(project, merge_request) expect(helper.markdown(actual)).to match(expected) end it "links to the commit" do - expected = project_commit_path(project, commit) + expected = urls.project_commit_path(project, commit) expect(helper.markdown(actual)).to match(expected) end it "links to the issue" do - expected = project_issue_path(project, issue) + expected = urls.project_issue_path(project, issue) expect(helper.markdown(actual)).to match(expected) end end @@ -46,7 +46,7 @@ describe MarkupHelper do let(:second_issue) { create(:issue, project: second_project) } it 'links to the issue' do - expected = project_issue_path(second_project, second_issue) + expected = urls.project_issue_path(second_project, second_issue) expect(markdown(actual, project: second_project)).to match(expected) end end @@ -93,7 +93,7 @@ describe MarkupHelper do # First issue link expect(doc.css('a')[1].attr('href')) - .to eq project_issue_path(project, issues[0]) + .to eq urls.project_issue_path(project, issues[0]) expect(doc.css('a')[1].text).to eq issues[0].to_reference # Internal commit link @@ -102,7 +102,7 @@ describe MarkupHelper do # Second issue link expect(doc.css('a')[3].attr('href')) - .to eq project_issue_path(project, issues[1]) + .to eq urls.project_issue_path(project, issues[1]) expect(doc.css('a')[3].text).to eq issues[1].to_reference # Trailing commit link @@ -128,7 +128,7 @@ describe MarkupHelper do # First issue link expect(doc.css('a')[1].attr('href')) - .to eq project_issue_path(project, issues[0]) + .to eq urls.project_issue_path(project, issues[0]) expect(doc.css('a')[1].text).to eq issues[0].to_reference # Internal commit link @@ -137,7 +137,7 @@ describe MarkupHelper do # Second issue link expect(doc.css('a')[3].attr('href')) - .to eq project_issue_path(project, issues[1]) + .to eq urls.project_issue_path(project, issues[1]) expect(doc.css('a')[3].text).to eq issues[1].to_reference # Trailing commit link @@ -183,7 +183,7 @@ describe MarkupHelper do doc = Nokogiri::HTML.parse(rendered) expect(doc.css('a')[0].attr('href')) - .to eq project_issue_path(project, issue) + .to eq urls.project_issue_path(project, issue) expect(doc.css('a')[0].text).to eq issue.to_reference wrapped = helper.link_to_html(rendered, link) @@ -207,6 +207,17 @@ describe MarkupHelper do expect(helper).to receive(:markdown_unsafe).with('wiki content', pipeline: :wiki, project: project, project_wiki: @wiki, page_slug: "nested/page", + issuable_state_filter_enabled: true) + + helper.render_wiki_content(@wiki) + end + + it 'uses Wiki pipeline for markdown files with RedCarpet if feature disabled' do + stub_feature_flags(commonmark_for_repositories: false) + allow(@wiki).to receive(:format).and_return(:markdown) + + expect(helper).to receive(:markdown_unsafe).with('wiki content', + pipeline: :wiki, project: project, project_wiki: @wiki, page_slug: "nested/page", issuable_state_filter_enabled: true, markdown_engine: :redcarpet) helper.render_wiki_content(@wiki) @@ -259,10 +270,18 @@ describe MarkupHelper do expect(helper.markup('foo.md', content, rendered: '<p>NOEL</p>')).to eq('<p>NOEL</p>') end - it 'defaults to Redcarpet' do - expect(helper).to receive(:markdown_unsafe).with(content, hash_including(markdown_engine: :redcarpet)).and_return('NOEL') + it 'defaults to CommonMark' do + expect(helper.markup('foo.md', 'x^2')).to include('x^2') + end - expect(helper.markup('foo.md', content)).to eq('NOEL') + it 'honors markdown_engine for RedCarpet' do + expect(helper.markup('foo.md', 'x^2', { markdown_engine: :redcarpet })).to include('x<sup>2</sup>') + end + + it 'uses RedCarpet if feature disabled' do + stub_feature_flags(commonmark_for_repositories: false) + + expect(helper.markup('foo.md', 'x^2', { markdown_engine: :redcarpet })).to include('x<sup>2</sup>') end end @@ -414,4 +433,8 @@ describe MarkupHelper do expect(helper.cross_project_reference(project, issue)).to include(project.full_path) end end + + def urls + Gitlab::Routing.url_helpers + end end diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb index cbd4ff0fb4a..976b6c312b4 100644 --- a/spec/helpers/projects_helper_spec.rb +++ b/spec/helpers/projects_helper_spec.rb @@ -470,4 +470,16 @@ describe ProjectsHelper do end end end + + describe '#legacy_render_context' do + it 'returns the redcarpet engine' do + params = { legacy_render: '1' } + + expect(helper.legacy_render_context(params)).to include(markdown_engine: :redcarpet) + end + + it 'returns nothing' do + expect(helper.legacy_render_context({})).to be_empty + end + end end diff --git a/spec/javascripts/fixtures/projects.rb b/spec/javascripts/fixtures/projects.rb index 57c78182abc..d98f7f55b20 100644 --- a/spec/javascripts/fixtures/projects.rb +++ b/spec/javascripts/fixtures/projects.rb @@ -6,6 +6,7 @@ describe 'Projects (JavaScript fixtures)', type: :controller do let(:admin) { create(:admin) } let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} let(:project) { create(:project, namespace: namespace, path: 'builds-project') } + let(:project_with_repo) { create(:project, :repository, description: 'Code and stuff') } let(:project_variable_populated) { create(:project, namespace: namespace, path: 'builds-project2') } let!(:variable1) { create(:ci_variable, project: project_variable_populated) } let!(:variable2) { create(:ci_variable, project: project_variable_populated) } @@ -35,6 +36,15 @@ describe 'Projects (JavaScript fixtures)', type: :controller do store_frontend_fixture(response, example.description) end + it 'projects/overview.html.raw' do |example| + get :show, + namespace_id: project_with_repo.namespace.to_param, + id: project_with_repo + + expect(response).to be_success + store_frontend_fixture(response, example.description) + end + it 'projects/edit.html.raw' do |example| get :edit, namespace_id: project.namespace.to_param, diff --git a/spec/javascripts/read_more_spec.js b/spec/javascripts/read_more_spec.js new file mode 100644 index 00000000000..b1af0f80a50 --- /dev/null +++ b/spec/javascripts/read_more_spec.js @@ -0,0 +1,23 @@ +import initReadMore from '~/read_more'; + +describe('Read more click-to-expand functionality', () => { + const fixtureName = 'projects/overview.html.raw'; + + preloadFixtures(fixtureName); + + beforeEach(() => { + loadFixtures(fixtureName); + }); + + describe('expands target element', () => { + it('adds "is-expanded" class to target element', () => { + const target = document.querySelector('.read-more-container'); + const trigger = document.querySelector('.js-read-more-trigger'); + initReadMore(); + + trigger.click(); + + expect(target.classList.contains('is-expanded')).toEqual(true); + }); + }); +}); diff --git a/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb b/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb index 88ae4c1e07a..52b8c9be647 100644 --- a/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb +++ b/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb @@ -121,6 +121,13 @@ describe Banzai::Pipeline::WikiPipeline do expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/page\"") end + it 'rewrites non-file links (with spaces) to be at the scope of the wiki root' do + markdown = "[Link to Page](page slug)" + output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug) + + expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/page%20slug\"") + end + it "rewrites file links to be at the scope of the current directory" do markdown = "[Link to Page](page.md)" output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug) @@ -134,6 +141,13 @@ describe Banzai::Pipeline::WikiPipeline do expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/start-page#title\"") end + + it 'rewrites links (with spaces) with anchor' do + markdown = '[Link to Header](start page#title)' + output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug) + + expect(output).to include("href=\"#{relative_url_root}/wiki_link_ns/wiki_link_project/wikis/start%20page#title\"") + end end describe "when creating root links" do diff --git a/spec/lib/feature_spec.rb b/spec/lib/feature_spec.rb index 8bb5a843484..48c0ba8a653 100644 --- a/spec/lib/feature_spec.rb +++ b/spec/lib/feature_spec.rb @@ -119,6 +119,10 @@ describe Feature do expect(described_class.enabled?(:some_random_feature_flag)).to be_falsey end + it 'returns true for undefined feature with default_enabled' do + expect(described_class.enabled?(:some_random_feature_flag, default_enabled: true)).to be_truthy + end + it 'returns false for existing disabled feature in the database' do described_class.disable(:disabled_feature_flag) @@ -160,6 +164,10 @@ describe Feature do expect(described_class.disabled?(:some_random_feature_flag)).to be_truthy end + it 'returns false for undefined feature with default_enabled' do + expect(described_class.disabled?(:some_random_feature_flag, default_enabled: true)).to be_falsey + end + it 'returns true for existing disabled feature in the database' do described_class.disable(:disabled_feature_flag) diff --git a/spec/lib/gitlab/ci/config/entry/job_spec.rb b/spec/lib/gitlab/ci/config/entry/job_spec.rb index 6769f64f950..2c9758401b7 100644 --- a/spec/lib/gitlab/ci/config/entry/job_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/job_spec.rb @@ -1,4 +1,5 @@ -require 'spec_helper' +require 'fast_spec_helper' +require_dependency 'active_model' describe Gitlab::Ci::Config::Entry::Job do let(:entry) { described_class.new(config, name: :rspec) } @@ -81,6 +82,15 @@ describe Gitlab::Ci::Config::Entry::Job do end end + context 'when extends key is not a string' do + let(:config) { { extends: 123 } } + + it 'returns error about wrong value type' do + expect(entry).not_to be_valid + expect(entry.errors).to include "job extends should be a string" + end + end + context 'when retry value is not correct' do context 'when it is not a numeric value' do let(:config) { { retry: true } } @@ -124,6 +134,8 @@ describe Gitlab::Ci::Config::Entry::Job do describe '#relevant?' do it 'is a relevant entry' do + entry = described_class.new({ script: 'rspec' }, name: :rspec) + expect(entry).to be_relevant end end diff --git a/spec/lib/gitlab/ci/config/extendable/entry_spec.rb b/spec/lib/gitlab/ci/config/extendable/entry_spec.rb new file mode 100644 index 00000000000..0a148375d11 --- /dev/null +++ b/spec/lib/gitlab/ci/config/extendable/entry_spec.rb @@ -0,0 +1,227 @@ +require 'fast_spec_helper' + +describe Gitlab::Ci::Config::Extendable::Entry do + describe '.new' do + context 'when entry key is not included in the context hash' do + it 'raises error' do + expect { described_class.new(:test, something: 'something') } + .to raise_error StandardError, 'Invalid entry key!' + end + end + end + + describe '#value' do + it 'reads a hash value from the context' do + entry = described_class.new(:test, test: 'something') + + expect(entry.value).to eq 'something' + end + end + + describe '#extensible?' do + context 'when entry has inheritance defined' do + it 'is extensible' do + entry = described_class.new(:test, test: { extends: 'something' }) + + expect(entry).to be_extensible + end + end + + context 'when entry does not have inheritance specified' do + it 'is not extensible' do + entry = described_class.new(:test, test: { script: 'something' }) + + expect(entry).not_to be_extensible + end + end + + context 'when entry value is not a hash' do + it 'is not extensible' do + entry = described_class.new(:test, test: 'something') + + expect(entry).not_to be_extensible + end + end + end + + describe '#extends_key' do + context 'when entry is extensible' do + it 'returns symbolized extends key value' do + entry = described_class.new(:test, test: { extends: 'something' }) + + expect(entry.extends_key).to eq :something + end + end + + context 'when entry is not extensible' do + it 'returns nil' do + entry = described_class.new(:test, test: 'something') + + expect(entry.extends_key).to be_nil + end + end + end + + describe '#ancestors' do + let(:parent) do + described_class.new(:test, test: { extends: 'something' }) + end + + let(:child) do + described_class.new(:job, { job: { script: 'something' } }, parent) + end + + it 'returns ancestors keys' do + expect(child.ancestors).to eq [:test] + end + end + + describe '#base_hash!' do + subject { described_class.new(:test, hash) } + + context 'when base hash is not extensible' do + let(:hash) do + { + template: { script: 'rspec' }, + test: { extends: 'template' } + } + end + + it 'returns unchanged base hash' do + expect(subject.base_hash!).to eq(script: 'rspec') + end + end + + context 'when base hash is extensible too' do + let(:hash) do + { + first: { script: 'rspec' }, + second: { extends: 'first' }, + test: { extends: 'second' } + } + end + + it 'extends the base hash first' do + expect(subject.base_hash!).to eq(extends: 'first', script: 'rspec') + end + + it 'mutates original context' do + subject.base_hash! + + expect(hash.fetch(:second)).to eq(extends: 'first', script: 'rspec') + end + end + end + + describe '#extend!' do + subject { described_class.new(:test, hash) } + + context 'when extending a non-hash value' do + let(:hash) do + { + first: 'my value', + test: { extends: 'first' } + } + end + + it 'raises an error' do + expect { subject.extend! } + .to raise_error(described_class::InvalidExtensionError, + /invalid base hash/) + end + end + + context 'when extending unknown key' do + let(:hash) do + { test: { extends: 'something' } } + end + + it 'raises an error' do + expect { subject.extend! } + .to raise_error(described_class::InvalidExtensionError, + /unknown key/) + end + end + + context 'when extending a hash correctly' do + let(:hash) do + { + first: { script: 'my value' }, + second: { extends: 'first' }, + test: { extends: 'second' } + } + end + + let(:result) do + { + first: { script: 'my value' }, + second: { extends: 'first', script: 'my value' }, + test: { extends: 'second', script: 'my value' } + } + end + + it 'returns extended part of the hash' do + expect(subject.extend!).to eq result[:test] + end + + it 'mutates original context' do + subject.extend! + + expect(hash).to eq result + end + end + + context 'when hash is not extensible' do + let(:hash) do + { + first: { script: 'my value' }, + second: { extends: 'first' }, + test: { value: 'something' } + } + end + + it 'returns original key value' do + expect(subject.extend!).to eq(value: 'something') + end + + it 'does not mutate orignal context' do + original = hash.deep_dup + + subject.extend! + + expect(hash).to eq original + end + end + + context 'when circular depenency gets detected' do + let(:hash) do + { test: { extends: 'test' } } + end + + it 'raises an error' do + expect { subject.extend! } + .to raise_error(described_class::CircularDependencyError, + /circular dependency detected/) + end + end + + context 'when nesting level is too deep' do + before do + stub_const("#{described_class}::MAX_NESTING_LEVELS", 0) + end + + let(:hash) do + { + first: { script: 'my value' }, + second: { extends: 'first' }, + test: { extends: 'second' } + } + end + + it 'raises an error' do + expect { subject.extend! } + .to raise_error(described_class::NestingTooDeepError) + end + end + end +end diff --git a/spec/lib/gitlab/ci/config/extendable_spec.rb b/spec/lib/gitlab/ci/config/extendable_spec.rb new file mode 100644 index 00000000000..90213f6603d --- /dev/null +++ b/spec/lib/gitlab/ci/config/extendable_spec.rb @@ -0,0 +1,228 @@ +require 'fast_spec_helper' + +describe Gitlab::Ci::Config::Extendable do + subject { described_class.new(hash) } + + describe '#each' do + context 'when there is extendable entry in the hash' do + let(:test) do + { extends: 'something', only: %w[master] } + end + + let(:hash) do + { something: { script: 'ls' }, test: test } + end + + it 'yields control' do + expect { |b| subject.each(&b) }.to yield_control + end + end + end + + describe '#to_hash' do + context 'when hash does not contain extensions' do + let(:hash) do + { + test: { script: 'test' }, + production: { + script: 'deploy', + only: { variables: %w[$SOMETHING] } + } + } + end + + it 'does not modify the hash' do + expect(subject.to_hash).to eq hash + end + end + + context 'when hash has a single simple extension' do + let(:hash) do + { + something: { + script: 'deploy', + only: { variables: %w[$SOMETHING] } + }, + + test: { + extends: 'something', + script: 'ls', + only: { refs: %w[master] } + } + } + end + + it 'extends a hash with a deep reverse merge' do + expect(subject.to_hash).to eq( + something: { + script: 'deploy', + only: { variables: %w[$SOMETHING] } + }, + + test: { + extends: 'something', + script: 'ls', + only: { + refs: %w[master], + variables: %w[$SOMETHING] + } + } + ) + end + end + + context 'when a hash uses recursive extensions' do + let(:hash) do + { + test: { + extends: 'something', + script: 'ls', + only: { refs: %w[master] } + }, + + build: { + extends: 'something', + stage: 'build' + }, + + deploy: { + stage: 'deploy', + extends: '.first' + }, + + something: { + extends: '.first', + script: 'exec', + only: { variables: %w[$SOMETHING] } + }, + + '.first': { + script: 'run', + only: { kubernetes: 'active' } + } + } + end + + it 'extends a hash with a deep reverse merge' do + expect(subject.to_hash).to eq( + '.first': { + script: 'run', + only: { kubernetes: 'active' } + }, + + something: { + extends: '.first', + script: 'exec', + only: { + kubernetes: 'active', + variables: %w[$SOMETHING] + } + }, + + deploy: { + script: 'run', + stage: 'deploy', + only: { kubernetes: 'active' }, + extends: '.first' + }, + + build: { + extends: 'something', + script: 'exec', + stage: 'build', + only: { + kubernetes: 'active', + variables: %w[$SOMETHING] + } + }, + + test: { + extends: 'something', + script: 'ls', + only: { + refs: %w[master], + variables: %w[$SOMETHING], + kubernetes: 'active' + } + } + ) + end + end + + context 'when nested circular dependecy has been detected' do + let(:hash) do + { + test: { + extends: 'something', + script: 'ls', + only: { refs: %w[master] } + }, + + something: { + extends: '.first', + script: 'deploy', + only: { variables: %w[$SOMETHING] } + }, + + '.first': { + extends: 'something', + script: 'run', + only: { kubernetes: 'active' } + } + } + end + + it 'raises an error about circular dependency' do + expect { subject.to_hash } + .to raise_error(described_class::Entry::CircularDependencyError) + end + end + + context 'when circular dependecy to self has been detected' do + let(:hash) do + { + test: { + extends: 'test', + script: 'ls', + only: { refs: %w[master] } + } + } + end + + it 'raises an error about circular dependency' do + expect { subject.to_hash } + .to raise_error(described_class::Entry::CircularDependencyError) + end + end + + context 'when invalid extends value is specified' do + let(:hash) do + { something: { extends: 1, script: 'ls' } } + end + + it 'raises an error about invalid extension' do + expect { subject.to_hash } + .to raise_error(described_class::Entry::InvalidExtensionError) + end + end + + context 'when extensible entry has non-hash inheritance defined' do + let(:hash) do + { + test: { + extends: 'something', + script: 'ls', + only: { refs: %w[master] } + }, + + something: 'some text' + } + end + + it 'raises an error about invalid base' do + expect { subject.to_hash } + .to raise_error(described_class::Entry::InvalidExtensionError) + end + end + end +end diff --git a/spec/lib/gitlab/ci/config_spec.rb b/spec/lib/gitlab/ci/config_spec.rb index 2e204da307d..5a78ce783dd 100644 --- a/spec/lib/gitlab/ci/config_spec.rb +++ b/spec/lib/gitlab/ci/config_spec.rb @@ -1,4 +1,6 @@ -require 'spec_helper' +require 'fast_spec_helper' + +require_dependency 'active_model' describe Gitlab::Ci::Config do let(:config) do @@ -42,6 +44,36 @@ describe Gitlab::Ci::Config do end end + context 'when using extendable hash' do + let(:yml) do + <<-EOS + image: ruby:2.2 + + rspec: + script: rspec + + test: + extends: rspec + image: ruby:alpine + EOS + end + + it 'correctly extends the hash' do + hash = { + image: 'ruby:2.2', + rspec: { script: 'rspec' }, + test: { + extends: 'rspec', + image: 'ruby:alpine', + script: 'rspec' + } + } + + expect(config).to be_valid + expect(config.to_hash).to eq hash + end + end + context 'when config is invalid' do context 'when yml is incorrect' do let(:yml) { '// invalid' } @@ -49,7 +81,7 @@ describe Gitlab::Ci::Config do describe '.new' do it 'raises error' do expect { config }.to raise_error( - ::Gitlab::Ci::Config::Loader::FormatError, + described_class::ConfigError, /Invalid configuration format/ ) end @@ -75,5 +107,21 @@ describe Gitlab::Ci::Config do end end end + + context 'when invalid extended hash has been provided' do + let(:yml) do + <<-EOS + test: + extends: test + script: rspec + EOS + end + + it 'raises an error' do + expect { config }.to raise_error( + described_class::ConfigError, /circular dependency detected/ + ) + end + end end end diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb index e73cdc54a15..fcbdf71a4e9 100644 --- a/spec/lib/gitlab/ci/yaml_processor_spec.rb +++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb @@ -562,6 +562,58 @@ module Gitlab end end + context 'when using `extends`' do + let(:config_processor) { Gitlab::Ci::YamlProcessor.new(config) } + + subject { config_processor.builds.first } + + context 'when using simple `extends`' do + let(:config) do + <<~YAML + .template: + script: test + + rspec: + extends: .template + image: ruby:alpine + YAML + end + + it 'correctly extends rspec job' do + expect(config_processor.builds).to be_one + expect(subject.dig(:commands)).to eq 'test' + expect(subject.dig(:options, :image, :name)).to eq 'ruby:alpine' + end + end + + context 'when using recursive `extends`' do + let(:config) do + <<~YAML + rspec: + extends: .test + script: rspec + when: always + + .template: + before_script: + - bundle install + + .test: + extends: .template + script: test + image: image:test + YAML + end + + it 'correctly extends rspec job' do + expect(config_processor.builds).to be_one + expect(subject.dig(:commands)).to eq "bundle install\nrspec" + expect(subject.dig(:options, :image, :name)).to eq 'image:test' + expect(subject.dig(:when)).to eq 'always' + end + end + end + describe "When" do %w(on_success on_failure always).each do |when_state| it "returns #{when_state} when defined" do @@ -1309,6 +1361,14 @@ module Gitlab .to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, 'jobs:rspec:only variables invalid expression syntax') end + + it 'returns errors if extended hash configuration is invalid' do + config = YAML.dump({ rspec: { extends: 'something', script: 'test' } }) + + expect { Gitlab::Ci::YamlProcessor.new(config) } + .to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, + 'rspec: unknown key in `extends`') + end end describe "Validate configuration templates" do diff --git a/spec/lib/gitlab/file_detector_spec.rb b/spec/lib/gitlab/file_detector_spec.rb index 8e524f9b05a..9e351368b22 100644 --- a/spec/lib/gitlab/file_detector_spec.rb +++ b/spec/lib/gitlab/file_detector_spec.rb @@ -29,11 +29,15 @@ describe Gitlab::FileDetector do end it 'returns the type of a license file' do - %w(LICENSE LICENCE COPYING).each do |file| + %w(LICENSE LICENCE COPYING UNLICENSE UNLICENCE).each do |file| expect(described_class.type_of(file)).to eq(:license) end end + it 'returns nil for an UNCOPYING file' do + expect(described_class.type_of('UNCOPYING')).to be_nil + end + it 'returns the type of a version file' do expect(described_class.type_of('VERSION')).to eq(:version) end diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index 0a1e3eb83d3..579f175c4a8 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -416,6 +416,7 @@ ProjectHook: - type - service_id - push_events +- push_events_branch_filter - issues_events - merge_requests_events - tag_push_events diff --git a/spec/lib/gitlab/kubernetes/cluster_role_binding_spec.rb b/spec/lib/gitlab/kubernetes/cluster_role_binding_spec.rb new file mode 100644 index 00000000000..4a669408025 --- /dev/null +++ b/spec/lib/gitlab/kubernetes/cluster_role_binding_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Kubernetes::ClusterRoleBinding do + let(:cluster_role_binding) { described_class.new(name, cluster_role_name, subjects) } + let(:name) { 'cluster-role-binding-name' } + let(:cluster_role_name) { 'cluster-admin' } + + let(:subjects) { [{ kind: 'ServiceAccount', name: 'sa', namespace: 'ns' }] } + + describe '#generate' do + let(:role_ref) do + { + apiGroup: 'rbac.authorization.k8s.io', + kind: 'ClusterRole', + name: cluster_role_name + } + end + + let(:resource) do + ::Kubeclient::Resource.new( + metadata: { name: name }, + roleRef: role_ref, + subjects: subjects + ) + end + + subject { cluster_role_binding.generate } + + it 'should build a Kubeclient Resource' do + is_expected.to eq(resource) + end + end +end diff --git a/spec/lib/gitlab/kubernetes/helm/api_spec.rb b/spec/lib/gitlab/kubernetes/helm/api_spec.rb index 341f71a3e49..25c3b37753d 100644 --- a/spec/lib/gitlab/kubernetes/helm/api_spec.rb +++ b/spec/lib/gitlab/kubernetes/helm/api_spec.rb @@ -5,9 +5,18 @@ describe Gitlab::Kubernetes::Helm::Api do let(:helm) { described_class.new(client) } let(:gitlab_namespace) { Gitlab::Kubernetes::Helm::NAMESPACE } let(:namespace) { Gitlab::Kubernetes::Namespace.new(gitlab_namespace, client) } - let(:application) { create(:clusters_applications_prometheus) } - - let(:command) { application.install_command } + let(:application_name) { 'app-name' } + let(:rbac) { false } + let(:files) { {} } + + let(:command) do + Gitlab::Kubernetes::Helm::InstallCommand.new( + name: application_name, + chart: 'chart-name', + rbac: rbac, + files: files + ) + end subject { helm } @@ -28,6 +37,8 @@ describe Gitlab::Kubernetes::Helm::Api do before do allow(client).to receive(:create_pod).and_return(nil) allow(client).to receive(:create_config_map).and_return(nil) + allow(client).to receive(:create_service_account).and_return(nil) + allow(client).to receive(:create_cluster_role_binding).and_return(nil) allow(namespace).to receive(:ensure_exists!).once end @@ -39,7 +50,7 @@ describe Gitlab::Kubernetes::Helm::Api do end context 'with a ConfigMap' do - let(:resource) { Gitlab::Kubernetes::ConfigMap.new(application.name, application.files).generate } + let(:resource) { Gitlab::Kubernetes::ConfigMap.new(application_name, files).generate } it 'creates a ConfigMap on kubeclient' do expect(client).to receive(:create_config_map).with(resource).once @@ -47,6 +58,96 @@ describe Gitlab::Kubernetes::Helm::Api do subject.install(command) end end + + context 'without a service account' do + it 'does not create a service account on kubeclient' do + expect(client).not_to receive(:create_service_account) + expect(client).not_to receive(:create_cluster_role_binding) + + subject.install(command) + end + end + + context 'with a service account' do + let(:command) { Gitlab::Kubernetes::Helm::InitCommand.new(name: application_name, files: files, rbac: rbac) } + + context 'rbac-enabled cluster' do + let(:rbac) { true } + + let(:service_account_resource) do + Kubeclient::Resource.new(metadata: { name: 'tiller', namespace: 'gitlab-managed-apps' }) + end + + let(:cluster_role_binding_resource) do + Kubeclient::Resource.new( + metadata: { name: 'tiller-admin' }, + roleRef: { apiGroup: 'rbac.authorization.k8s.io', kind: 'ClusterRole', name: 'cluster-admin' }, + subjects: [{ kind: 'ServiceAccount', name: 'tiller', namespace: 'gitlab-managed-apps' }] + ) + end + + context 'service account and cluster role binding does not exist' do + before do + expect(client).to receive('get_service_account').with('tiller', 'gitlab-managed-apps').and_raise(Kubeclient::HttpError.new(404, 'Not found', nil)) + expect(client).to receive('get_cluster_role_binding').with('tiller-admin').and_raise(Kubeclient::HttpError.new(404, 'Not found', nil)) + end + + it 'creates a service account, followed the cluster role binding on kubeclient' do + expect(client).to receive(:create_service_account).with(service_account_resource).once.ordered + expect(client).to receive(:create_cluster_role_binding).with(cluster_role_binding_resource).once.ordered + + subject.install(command) + end + end + + context 'service account already exists' do + before do + expect(client).to receive('get_service_account').with('tiller', 'gitlab-managed-apps').and_return(service_account_resource) + expect(client).to receive('get_cluster_role_binding').with('tiller-admin').and_raise(Kubeclient::HttpError.new(404, 'Not found', nil)) + end + + it 'updates the service account, followed by creating the cluster role binding' do + expect(client).to receive(:update_service_account).with(service_account_resource).once.ordered + expect(client).to receive(:create_cluster_role_binding).with(cluster_role_binding_resource).once.ordered + + subject.install(command) + end + end + + context 'service account and cluster role binding already exists' do + before do + expect(client).to receive('get_service_account').with('tiller', 'gitlab-managed-apps').and_return(service_account_resource) + expect(client).to receive('get_cluster_role_binding').with('tiller-admin').and_return(cluster_role_binding_resource) + end + + it 'updates the service account, followed by creating the cluster role binding' do + expect(client).to receive(:update_service_account).with(service_account_resource).once.ordered + expect(client).to receive(:update_cluster_role_binding).with(cluster_role_binding_resource).once.ordered + + subject.install(command) + end + end + + context 'a non-404 error is thrown' do + before do + expect(client).to receive('get_service_account').with('tiller', 'gitlab-managed-apps').and_raise(Kubeclient::HttpError.new(401, 'Unauthorized', nil)) + end + + it 'raises an error' do + expect { subject.install(command) }.to raise_error(Kubeclient::HttpError) + end + end + end + + context 'legacy abac cluster' do + it 'does not create a service account on kubeclient' do + expect(client).not_to receive(:create_service_account) + expect(client).not_to receive(:create_cluster_role_binding) + + subject.install(command) + end + end + end end describe '#status' do diff --git a/spec/lib/gitlab/kubernetes/helm/base_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/base_command_spec.rb index d50616e95e8..aacae78be43 100644 --- a/spec/lib/gitlab/kubernetes/helm/base_command_spec.rb +++ b/spec/lib/gitlab/kubernetes/helm/base_command_spec.rb @@ -2,14 +2,24 @@ require 'spec_helper' describe Gitlab::Kubernetes::Helm::BaseCommand do let(:application) { create(:clusters_applications_helm) } + let(:rbac) { false } + let(:test_class) do Class.new do include Gitlab::Kubernetes::Helm::BaseCommand + def initialize(rbac) + @rbac = rbac + end + def name "test-class-name" end + def rbac? + @rbac + end + def files { some: 'value' @@ -19,7 +29,7 @@ describe Gitlab::Kubernetes::Helm::BaseCommand do end let(:base_command) do - test_class.new + test_class.new(rbac) end subject { base_command } @@ -34,6 +44,14 @@ describe Gitlab::Kubernetes::Helm::BaseCommand do it 'should returns a kubeclient resoure with pod content for application' do is_expected.to be_an_instance_of ::Kubeclient::Resource end + + context 'when rbac is true' do + let(:rbac) { true } + + it 'also returns a kubeclient resource' do + is_expected.to be_an_instance_of ::Kubeclient::Resource + end + end end describe '#pod_name' do diff --git a/spec/lib/gitlab/kubernetes/helm/init_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/init_command_spec.rb index dcbc046cf00..72dc1817936 100644 --- a/spec/lib/gitlab/kubernetes/helm/init_command_spec.rb +++ b/spec/lib/gitlab/kubernetes/helm/init_command_spec.rb @@ -2,9 +2,135 @@ require 'spec_helper' describe Gitlab::Kubernetes::Helm::InitCommand do let(:application) { create(:clusters_applications_helm) } - let(:commands) { 'helm init --tiller-tls --tiller-tls-verify --tls-ca-cert /data/helm/helm/config/ca.pem --tiller-tls-cert /data/helm/helm/config/cert.pem --tiller-tls-key /data/helm/helm/config/key.pem >/dev/null' } + let(:rbac) { false } + let(:files) { {} } + let(:init_command) { described_class.new(name: application.name, files: files, rbac: rbac) } - subject { described_class.new(name: application.name, files: {}) } + let(:commands) do + <<~EOS + helm init --tiller-tls --tiller-tls-verify --tls-ca-cert /data/helm/helm/config/ca.pem --tiller-tls-cert /data/helm/helm/config/cert.pem --tiller-tls-key /data/helm/helm/config/key.pem >/dev/null + EOS + end + + subject { init_command } it_behaves_like 'helm commands' + + context 'on a rbac-enabled cluster' do + let(:rbac) { true } + + it_behaves_like 'helm commands' do + let(:commands) do + <<~EOS + helm init --tiller-tls --tiller-tls-verify --tls-ca-cert /data/helm/helm/config/ca.pem --tiller-tls-cert /data/helm/helm/config/cert.pem --tiller-tls-key /data/helm/helm/config/key.pem --service-account tiller >/dev/null + EOS + end + end + end + + describe '#rbac?' do + subject { init_command.rbac? } + + context 'rbac is enabled' do + let(:rbac) { true } + + it { is_expected.to be_truthy } + end + + context 'rbac is not enabled' do + let(:rbac) { false } + + it { is_expected.to be_falsey } + end + end + + describe '#config_map_resource' do + let(:metadata) do + { + name: 'values-content-configuration-helm', + namespace: 'gitlab-managed-apps', + labels: { name: 'values-content-configuration-helm' } + } + end + + let(:resource) { ::Kubeclient::Resource.new(metadata: metadata, data: files) } + + subject { init_command.config_map_resource } + + it 'returns a KubeClient resource with config map content for the application' do + is_expected.to eq(resource) + end + end + + describe '#pod_resource' do + subject { init_command.pod_resource } + + context 'rbac is enabled' do + let(:rbac) { true } + + it 'generates a pod that uses the tiller serviceAccountName' do + expect(subject.spec.serviceAccountName).to eq('tiller') + end + end + + context 'rbac is not enabled' do + let(:rbac) { false } + + it 'generates a pod that uses the default serviceAccountName' do + expect(subject.spec.serviceAcccountName).to be_nil + end + end + end + + describe '#service_account_resource' do + let(:resource) do + Kubeclient::Resource.new(metadata: { name: 'tiller', namespace: 'gitlab-managed-apps' }) + end + + subject { init_command.service_account_resource } + + context 'rbac is enabled' do + let(:rbac) { true } + + it 'generates a Kubeclient resource for the tiller ServiceAccount' do + is_expected.to eq(resource) + end + end + + context 'rbac is not enabled' do + let(:rbac) { false } + + it 'generates nothing' do + is_expected.to be_nil + end + end + end + + describe '#cluster_role_binding_resource' do + let(:resource) do + Kubeclient::Resource.new( + metadata: { name: 'tiller-admin' }, + roleRef: { apiGroup: 'rbac.authorization.k8s.io', kind: 'ClusterRole', name: 'cluster-admin' }, + subjects: [{ kind: 'ServiceAccount', name: 'tiller', namespace: 'gitlab-managed-apps' }] + ) + end + + subject { init_command.cluster_role_binding_resource } + + context 'rbac is enabled' do + let(:rbac) { true } + + it 'generates a Kubeclient resource for the ClusterRoleBinding for tiller' do + is_expected.to eq(resource) + end + end + + context 'rbac is not enabled' do + let(:rbac) { false } + + it 'generates nothing' do + is_expected.to be_nil + end + end + end end diff --git a/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb index 982e2f41043..f28941ce58f 100644 --- a/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb +++ b/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb @@ -3,14 +3,17 @@ require 'rails_helper' describe Gitlab::Kubernetes::Helm::InstallCommand do let(:files) { { 'ca.pem': 'some file content' } } let(:repository) { 'https://repository.example.com' } + let(:rbac) { false } let(:version) { '1.2.3' } let(:install_command) do described_class.new( name: 'app-name', chart: 'chart-name', + rbac: rbac, files: files, - version: version, repository: repository + version: version, + repository: repository ) end @@ -21,19 +24,76 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do <<~EOS helm init --client-only >/dev/null helm repo add app-name https://repository.example.com - helm install chart-name --name app-name --tls --tls-ca-cert /data/helm/app-name/config/ca.pem --tls-cert /data/helm/app-name/config/cert.pem --tls-key /data/helm/app-name/config/key.pem --version 1.2.3 --namespace gitlab-managed-apps -f /data/helm/app-name/config/values.yaml >/dev/null + #{helm_install_comand} + EOS + end + + let(:helm_install_comand) do + <<~EOS.squish + helm install chart-name + --name app-name + --tls + --tls-ca-cert /data/helm/app-name/config/ca.pem + --tls-cert /data/helm/app-name/config/cert.pem + --tls-key /data/helm/app-name/config/key.pem + --version 1.2.3 + --namespace gitlab-managed-apps + -f /data/helm/app-name/config/values.yaml >/dev/null EOS end end + context 'when rbac is true' do + let(:rbac) { true } + + it_behaves_like 'helm commands' do + let(:commands) do + <<~EOS + helm init --client-only >/dev/null + helm repo add app-name https://repository.example.com + #{helm_install_command} + EOS + end + + let(:helm_install_command) do + <<~EOS.squish + helm install chart-name + --name app-name + --tls + --tls-ca-cert /data/helm/app-name/config/ca.pem + --tls-cert /data/helm/app-name/config/cert.pem + --tls-key /data/helm/app-name/config/key.pem + --version 1.2.3 + --set rbac.create\\=true,rbac.enabled\\=true + --namespace gitlab-managed-apps + -f /data/helm/app-name/config/values.yaml >/dev/null + EOS + end + end + end + context 'when there is no repository' do let(:repository) { nil } it_behaves_like 'helm commands' do let(:commands) do <<~EOS - helm init --client-only >/dev/null - helm install chart-name --name app-name --tls --tls-ca-cert /data/helm/app-name/config/ca.pem --tls-cert /data/helm/app-name/config/cert.pem --tls-key /data/helm/app-name/config/key.pem --version 1.2.3 --namespace gitlab-managed-apps -f /data/helm/app-name/config/values.yaml >/dev/null + helm init --client-only >/dev/null + #{helm_install_command} + EOS + end + + let(:helm_install_command) do + <<~EOS.squish + helm install chart-name + --name app-name + --tls + --tls-ca-cert /data/helm/app-name/config/ca.pem + --tls-cert /data/helm/app-name/config/cert.pem + --tls-key /data/helm/app-name/config/key.pem + --version 1.2.3 + --namespace gitlab-managed-apps + -f /data/helm/app-name/config/values.yaml >/dev/null EOS end end @@ -45,9 +105,19 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do it_behaves_like 'helm commands' do let(:commands) do <<~EOS - helm init --client-only >/dev/null - helm repo add app-name https://repository.example.com - helm install chart-name --name app-name --version 1.2.3 --namespace gitlab-managed-apps -f /data/helm/app-name/config/values.yaml >/dev/null + helm init --client-only >/dev/null + helm repo add app-name https://repository.example.com + #{helm_install_command} + EOS + end + + let(:helm_install_command) do + <<~EOS.squish + helm install chart-name + --name app-name + --version 1.2.3 + --namespace gitlab-managed-apps + -f /data/helm/app-name/config/values.yaml >/dev/null EOS end end @@ -59,14 +129,63 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do it_behaves_like 'helm commands' do let(:commands) do <<~EOS - helm init --client-only >/dev/null - helm repo add app-name https://repository.example.com - helm install chart-name --name app-name --tls --tls-ca-cert /data/helm/app-name/config/ca.pem --tls-cert /data/helm/app-name/config/cert.pem --tls-key /data/helm/app-name/config/key.pem --namespace gitlab-managed-apps -f /data/helm/app-name/config/values.yaml >/dev/null + helm init --client-only >/dev/null + helm repo add app-name https://repository.example.com + #{helm_install_command} + EOS + end + + let(:helm_install_command) do + <<~EOS.squish + helm install chart-name + --name app-name + --tls + --tls-ca-cert /data/helm/app-name/config/ca.pem + --tls-cert /data/helm/app-name/config/cert.pem + --tls-key /data/helm/app-name/config/key.pem + --namespace gitlab-managed-apps + -f /data/helm/app-name/config/values.yaml >/dev/null EOS end end end + describe '#rbac?' do + subject { install_command.rbac? } + + context 'rbac is enabled' do + let(:rbac) { true } + + it { is_expected.to be_truthy } + end + + context 'rbac is not enabled' do + let(:rbac) { false } + + it { is_expected.to be_falsey } + end + end + + describe '#pod_resource' do + subject { install_command.pod_resource } + + context 'rbac is enabled' do + let(:rbac) { true } + + it 'generates a pod that uses the tiller serviceAccountName' do + expect(subject.spec.serviceAccountName).to eq('tiller') + end + end + + context 'rbac is not enabled' do + let(:rbac) { false } + + it 'generates a pod that uses the default serviceAccountName' do + expect(subject.spec.serviceAcccountName).to be_nil + end + end + end + describe '#config_map_resource' do let(:metadata) do { @@ -84,4 +203,20 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do is_expected.to eq(resource) end end + + describe '#service_account_resource' do + subject { install_command.service_account_resource } + + it 'returns nothing' do + is_expected.to be_nil + end + end + + describe '#cluster_role_binding_resource' do + subject { install_command.cluster_role_binding_resource } + + it 'returns nothing' do + is_expected.to be_nil + end + end end diff --git a/spec/lib/gitlab/kubernetes/helm/pod_spec.rb b/spec/lib/gitlab/kubernetes/helm/pod_spec.rb index ec64193c0b2..b333b334f36 100644 --- a/spec/lib/gitlab/kubernetes/helm/pod_spec.rb +++ b/spec/lib/gitlab/kubernetes/helm/pod_spec.rb @@ -5,8 +5,9 @@ describe Gitlab::Kubernetes::Helm::Pod do let(:app) { create(:clusters_applications_prometheus) } let(:command) { app.install_command } let(:namespace) { Gitlab::Kubernetes::Helm::NAMESPACE } + let(:service_account_name) { nil } - subject { described_class.new(command, namespace) } + subject { described_class.new(command, namespace, service_account_name: service_account_name) } context 'with a command' do it 'should generate a Kubeclient::Resource' do @@ -58,6 +59,20 @@ describe Gitlab::Kubernetes::Helm::Pod do expect(volume.configMap['items'].first['key']).to eq(:'values.yaml') expect(volume.configMap['items'].first['path']).to eq(:'values.yaml') end + + it 'should have no serviceAccountName' do + spec = subject.generate.spec + expect(spec.serviceAccountName).to be_nil + end + + context 'with a service_account_name' do + let(:service_account_name) { 'sa' } + + it 'should use the serviceAccountName provided' do + spec = subject.generate.spec + expect(spec.serviceAccountName).to eq(service_account_name) + end + end end end end diff --git a/spec/lib/gitlab/kubernetes/kube_client_spec.rb b/spec/lib/gitlab/kubernetes/kube_client_spec.rb new file mode 100644 index 00000000000..9146729d139 --- /dev/null +++ b/spec/lib/gitlab/kubernetes/kube_client_spec.rb @@ -0,0 +1,247 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Kubernetes::KubeClient do + include KubernetesHelpers + + let(:api_url) { 'https://kubernetes.example.com/prefix' } + let(:api_groups) { ['api', 'apis/rbac.authorization.k8s.io'] } + let(:api_version) { 'v1' } + let(:kubeclient_options) { { auth_options: { bearer_token: 'xyz' } } } + + let(:client) { described_class.new(api_url, api_groups, api_version, kubeclient_options) } + + before do + stub_kubeclient_discover(api_url) + end + + describe '#hashed_clients' do + subject { client.hashed_clients } + + it 'has keys from api groups' do + expect(subject.keys).to match_array api_groups + end + + it 'has values of Kubeclient::Client' do + expect(subject.values).to all(be_an_instance_of Kubeclient::Client) + end + end + + describe '#clients' do + subject { client.clients } + + it 'is not empty' do + is_expected.to be_present + end + + it 'is an array of Kubeclient::Client objects' do + is_expected.to all(be_an_instance_of Kubeclient::Client) + end + + it 'has each API group url' do + expected_urls = api_groups.map { |group| "#{api_url}/#{group}" } + + expect(subject.map(&:api_endpoint).map(&:to_s)).to match_array(expected_urls) + end + + it 'has the kubeclient options' do + subject.each do |client| + expect(client.auth_options).to eq({ bearer_token: 'xyz' }) + end + end + + it 'has the api_version' do + subject.each do |client| + expect(client.instance_variable_get(:@api_version)).to eq('v1') + end + end + end + + describe '#core_client' do + subject { client.core_client } + + it 'is a Kubeclient::Client' do + is_expected.to be_an_instance_of Kubeclient::Client + end + + it 'has the core API endpoint' do + expect(subject.api_endpoint.to_s).to match(%r{\/api\Z}) + end + end + + describe '#rbac_client' do + subject { client.rbac_client } + + it 'is a Kubeclient::Client' do + is_expected.to be_an_instance_of Kubeclient::Client + end + + it 'has the RBAC API group endpoint' do + expect(subject.api_endpoint.to_s).to match(%r{\/apis\/rbac.authorization.k8s.io\Z}) + end + end + + describe '#extensions_client' do + subject { client.extensions_client } + + let(:api_groups) { ['apis/extensions'] } + + it 'is a Kubeclient::Client' do + is_expected.to be_an_instance_of Kubeclient::Client + end + + it 'has the extensions API group endpoint' do + expect(subject.api_endpoint.to_s).to match(%r{\/apis\/extensions\Z}) + end + end + + describe '#discover!' do + it 'makes a discovery request for each API group' do + client.discover! + + api_groups.each do |api_group| + discovery_url = api_url + '/' + api_group + '/v1' + expect(WebMock).to have_requested(:get, discovery_url).once + end + end + end + + describe 'core API' do + let(:core_client) { client.core_client } + + [ + :get_pods, + :get_secrets, + :get_config_map, + :get_pod, + :get_namespace, + :get_service, + :get_service_account, + :delete_pod, + :create_config_map, + :create_namespace, + :create_pod, + :create_service_account, + :update_config_map, + :update_service_account + ].each do |method| + describe "##{method}" do + it 'delegates to the core client' do + expect(client).to delegate_method(method).to(:core_client) + end + + it 'responds to the method' do + expect(client).to respond_to method + end + end + end + end + + describe 'rbac API group' do + let(:rbac_client) { client.rbac_client } + + [ + :create_cluster_role_binding, + :get_cluster_role_binding, + :update_cluster_role_binding + ].each do |method| + describe "##{method}" do + it 'delegates to the rbac client' do + expect(client).to delegate_method(method).to(:rbac_client) + end + + it 'responds to the method' do + expect(client).to respond_to method + end + + context 'no rbac client' do + let(:api_groups) { ['api'] } + + it 'throws an error' do + expect { client.public_send(method) }.to raise_error(Module::DelegationError) + end + end + end + end + end + + describe 'extensions API group' do + let(:api_groups) { ['apis/extensions'] } + let(:api_version) { 'v1beta1' } + let(:extensions_client) { client.extensions_client } + + describe '#get_deployments' do + it 'delegates to the extensions client' do + expect(client).to delegate_method(:get_deployments).to(:extensions_client) + end + + it 'responds to the method' do + expect(client).to respond_to :get_deployments + end + + context 'no extensions client' do + let(:api_groups) { ['api'] } + let(:api_version) { 'v1' } + + it 'throws an error' do + expect { client.get_deployments }.to raise_error(Module::DelegationError) + end + end + end + end + + describe 'non-entity methods' do + it 'does not proxy for non-entity methods' do + expect(client.clients.first).to respond_to :proxy_url + + expect(client).not_to respond_to :proxy_url + end + + it 'throws an error' do + expect { client.proxy_url }.to raise_error(NoMethodError) + end + end + + describe '#get_pod_log' do + let(:core_client) { client.core_client } + + it 'is delegated to the core client' do + expect(client).to delegate_method(:get_pod_log).to(:core_client) + end + + context 'when no core client' do + let(:api_groups) { ['apis/extensions'] } + + it 'throws an error' do + expect { client.get_pod_log('pod-name') }.to raise_error(Module::DelegationError) + end + end + end + + describe '#watch_pod_log' do + let(:core_client) { client.core_client } + + it 'is delegated to the core client' do + expect(client).to delegate_method(:watch_pod_log).to(:core_client) + end + + context 'when no core client' do + let(:api_groups) { ['apis/extensions'] } + + it 'throws an error' do + expect { client.watch_pod_log('pod-name') }.to raise_error(Module::DelegationError) + end + end + end + + describe 'methods that do not exist on any client' do + it 'throws an error' do + expect { client.non_existent_method }.to raise_error(NoMethodError) + end + + it 'returns false for respond_to' do + expect(client.respond_to?(:non_existent_method)).to be_falsey + end + end +end diff --git a/spec/lib/gitlab/kubernetes/service_account_spec.rb b/spec/lib/gitlab/kubernetes/service_account_spec.rb new file mode 100644 index 00000000000..8da9e932dc3 --- /dev/null +++ b/spec/lib/gitlab/kubernetes/service_account_spec.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Kubernetes::ServiceAccount do + let(:name) { 'a_service_account' } + let(:namespace_name) { 'a_namespace' } + let(:service_account) { described_class.new(name, namespace_name) } + + it { expect(service_account.name).to eq(name) } + it { expect(service_account.namespace_name).to eq(namespace_name) } + + describe '#generate' do + let(:resource) do + ::Kubeclient::Resource.new(metadata: { name: name, namespace: namespace_name }) + end + + subject { service_account.generate } + + it 'should build a Kubeclient Resource' do + is_expected.to eq(resource) + end + end +end diff --git a/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb index 4e7bd433a9c..ee6d6fc961f 100644 --- a/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb +++ b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb @@ -42,6 +42,65 @@ describe Gitlab::Metrics::Subscribers::ActiveRecord do subscriber.sql(event) end + + context 'events are internal to Rails or irrelevant' do + let(:schema_event) do + double( + :event, + name: 'sql.active_record', + payload: { + sql: "SELECT attr.attname FROM pg_attribute attr INNER JOIN pg_constraint cons ON attr.attrelid = cons.conrelid AND attr.attnum = any(cons.conkey) WHERE cons.contype = 'p' AND cons.conrelid = '\"projects\"'::regclass", + name: 'SCHEMA', + connection_id: 135, + statement_name: nil, + binds: [] + }, + duration: 0.7 + ) + end + + let(:begin_event) do + double( + :event, + name: 'sql.active_record', + payload: { + sql: "BEGIN", + name: nil, + connection_id: 231, + statement_name: nil, + binds: [] + }, + duration: 1.1 + ) + end + + let(:commit_event) do + double( + :event, + name: 'sql.active_record', + payload: { + sql: "COMMIT", + name: nil, + connection_id: 212, + statement_name: nil, + binds: [] + }, + duration: 1.6 + ) + end + + it 'skips schema/begin/commit sql commands' do + expect(subscriber).to receive(:current_transaction) + .at_least(:once) + .and_return(transaction) + + expect(transaction).not_to receive(:increment) + + subscriber.sql(schema_event) + subscriber.sql(begin_event) + subscriber.sql(commit_event) + end + end end end end diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index de6dd2a9fea..1ec1fe10744 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -166,4 +166,20 @@ describe Gitlab::UsageData do expect(subject[:recorded_at]).to be_a(Time) end end + + describe '#count' do + let(:relation) { double(:relation) } + + it 'returns the count when counting succeeds' do + allow(relation).to receive(:count).and_return(1) + + expect(described_class.count(relation)).to eq(1) + end + + it 'returns the fallback value when counting fails' do + allow(relation).to receive(:count).and_raise(ActiveRecord::StatementInvalid.new('')) + + expect(described_class.count(relation, fallback: 15)).to eq(15) + end + end end diff --git a/spec/lib/gitlab/user_extractor_spec.rb b/spec/lib/gitlab/user_extractor_spec.rb new file mode 100644 index 00000000000..fcc05ab3a0c --- /dev/null +++ b/spec/lib/gitlab/user_extractor_spec.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::UserExtractor do + let(:text) do + <<~TXT + This is a long texth that mentions some users. + @user-1, @user-2 and user@gitlab.org take a walk in the park. + There they meet @user-4 that was out with other-user@gitlab.org. + @user-1 thought it was late, so went home straight away + TXT + end + subject(:extractor) { described_class.new(text) } + + describe '#users' do + it 'returns an empty relation when nil was passed' do + extractor = described_class.new(nil) + + expect(extractor.users).to be_empty + expect(extractor.users).to be_a(ActiveRecord::Relation) + end + + it 'returns the user case insensitive for usernames' do + user = create(:user, username: "USER-4") + + expect(extractor.users).to include(user) + end + + it 'returns users by primary email' do + user = create(:user, email: 'user@gitlab.org') + + expect(extractor.users).to include(user) + end + + it 'returns users by secondary email' do + user = create(:email, email: 'other-user@gitlab.org').user + + expect(extractor.users).to include(user) + end + end + + describe '#matches' do + it 'includes all mentioned email adresses' do + expect(extractor.matches[:emails]).to contain_exactly('user@gitlab.org', 'other-user@gitlab.org') + end + + it 'includes all mentioned usernames' do + expect(extractor.matches[:usernames]).to contain_exactly('user-1', 'user-2', 'user-4') + end + end + + describe '#references' do + it 'includes all user-references once' do + expect(extractor.references).to contain_exactly('user-1', 'user-2', 'user@gitlab.org', 'user-4', 'other-user@gitlab.org') + end + end +end diff --git a/spec/lib/object_storage/direct_upload_spec.rb b/spec/lib/object_storage/direct_upload_spec.rb index 632acd6eb46..9c308cc1be9 100644 --- a/spec/lib/object_storage/direct_upload_spec.rb +++ b/spec/lib/object_storage/direct_upload_spec.rb @@ -62,7 +62,7 @@ describe ObjectStorage::DirectUpload do expect(subject[:StoreURL]).to start_with(storage_url) expect(subject[:DeleteURL]).to start_with(storage_url) expect(subject[:CustomPutHeaders]).to be_truthy - expect(subject[:PutHeaders]).to eq({ 'Content-Type' => 'application/octet-stream' }) + expect(subject[:PutHeaders]).to eq({}) end end diff --git a/spec/models/clusters/applications/helm_spec.rb b/spec/models/clusters/applications/helm_spec.rb index e5b2bdc8a4e..2c37cd20ecc 100644 --- a/spec/models/clusters/applications/helm_spec.rb +++ b/spec/models/clusters/applications/helm_spec.rb @@ -47,5 +47,19 @@ describe Clusters::Applications::Helm do cert = OpenSSL::X509::Certificate.new(subject.files[:'cert.pem']) expect(cert.not_after).to be > 999.years.from_now end + + describe 'rbac' do + context 'non rbac cluster' do + it { expect(subject).not_to be_rbac } + end + + context 'rbac cluster' do + before do + helm.cluster.platform_kubernetes.rbac! + end + + it { expect(subject).to be_rbac } + end + end end end diff --git a/spec/models/clusters/applications/ingress_spec.rb b/spec/models/clusters/applications/ingress_spec.rb index 21f75ced8c3..c55953c8d22 100644 --- a/spec/models/clusters/applications/ingress_spec.rb +++ b/spec/models/clusters/applications/ingress_spec.rb @@ -88,9 +88,18 @@ describe Clusters::Applications::Ingress do expect(subject.name).to eq('ingress') expect(subject.chart).to eq('stable/nginx-ingress') expect(subject.version).to eq('0.23.0') + expect(subject).not_to be_rbac expect(subject.files).to eq(ingress.files) end + context 'on a rbac enabled cluster' do + before do + ingress.cluster.platform_kubernetes.rbac! + end + + it { is_expected.to be_rbac } + end + context 'application failed to install previously' do let(:ingress) { create(:clusters_applications_ingress, :errored, version: 'nginx') } diff --git a/spec/models/clusters/applications/jupyter_spec.rb b/spec/models/clusters/applications/jupyter_spec.rb index 027b732681b..591a01d78a9 100644 --- a/spec/models/clusters/applications/jupyter_spec.rb +++ b/spec/models/clusters/applications/jupyter_spec.rb @@ -51,10 +51,19 @@ describe Clusters::Applications::Jupyter do expect(subject.name).to eq('jupyter') expect(subject.chart).to eq('jupyter/jupyterhub') expect(subject.version).to eq('v0.6') + expect(subject).not_to be_rbac expect(subject.repository).to eq('https://jupyterhub.github.io/helm-chart/') expect(subject.files).to eq(jupyter.files) end + context 'on a rbac enabled cluster' do + before do + jupyter.cluster.platform_kubernetes.rbac! + end + + it { is_expected.to be_rbac } + end + context 'application failed to install previously' do let(:jupyter) { create(:clusters_applications_jupyter, :errored, version: '0.0.1') } diff --git a/spec/models/clusters/applications/prometheus_spec.rb b/spec/models/clusters/applications/prometheus_spec.rb index 26b75c75e1d..f34b4ece8db 100644 --- a/spec/models/clusters/applications/prometheus_spec.rb +++ b/spec/models/clusters/applications/prometheus_spec.rb @@ -1,6 +1,8 @@ require 'rails_helper' describe Clusters::Applications::Prometheus do + include KubernetesHelpers + include_examples 'cluster application core specs', :clusters_applications_prometheus include_examples 'cluster application status specs', :cluster_application_prometheus @@ -107,26 +109,14 @@ describe Clusters::Applications::Prometheus do end context 'cluster has kubeclient' do - let(:kubernetes_url) { 'http://example.com' } - let(:k8s_discover_response) do - { - resources: [ - { - name: 'service', - kind: 'Service' - } - ] - } - end - - let(:kube_client) { Kubeclient::Client.new(kubernetes_url) } + let(:kubernetes_url) { subject.cluster.platform_kubernetes.api_url } + let(:kube_client) { subject.cluster.kubeclient.core_client } - let(:cluster) { create(:cluster) } - subject { create(:clusters_applications_prometheus, cluster: cluster) } + subject { create(:clusters_applications_prometheus) } before do - allow(kube_client.rest_client).to receive(:get).and_return(k8s_discover_response.to_json) - allow(subject.cluster).to receive(:kubeclient).and_return(kube_client) + subject.cluster.platform_kubernetes.namespace = 'a-namespace' + stub_kubeclient_discover(subject.cluster.platform_kubernetes.api_url) end it 'creates proxy prometheus rest client' do @@ -134,7 +124,7 @@ describe Clusters::Applications::Prometheus do end it 'creates proper url' do - expect(subject.prometheus_client.url).to eq('http://example.com/api/v1/namespaces/gitlab-managed-apps/service/prometheus-prometheus-server:80/proxy') + expect(subject.prometheus_client.url).to eq("#{kubernetes_url}/api/v1/namespaces/gitlab-managed-apps/services/prometheus-prometheus-server:80/proxy") end it 'copies options and headers from kube client to proxy client' do @@ -164,9 +154,18 @@ describe Clusters::Applications::Prometheus do expect(subject.name).to eq('prometheus') expect(subject.chart).to eq('stable/prometheus') expect(subject.version).to eq('6.7.3') + expect(subject).not_to be_rbac expect(subject.files).to eq(prometheus.files) end + context 'on a rbac enabled cluster' do + before do + prometheus.cluster.platform_kubernetes.rbac! + end + + it { is_expected.to be_rbac } + end + context 'application failed to install previously' do let(:prometheus) { create(:clusters_applications_prometheus, :errored, version: '2.0.0') } diff --git a/spec/models/clusters/applications/runner_spec.rb b/spec/models/clusters/applications/runner_spec.rb index d84f125e246..eda8d519f60 100644 --- a/spec/models/clusters/applications/runner_spec.rb +++ b/spec/models/clusters/applications/runner_spec.rb @@ -46,10 +46,19 @@ describe Clusters::Applications::Runner do expect(subject.name).to eq('runner') expect(subject.chart).to eq('runner/gitlab-runner') expect(subject.version).to eq('0.1.31') + expect(subject).not_to be_rbac expect(subject.repository).to eq('https://charts.gitlab.io') expect(subject.files).to eq(gitlab_runner.files) end + context 'on a rbac enabled cluster' do + before do + gitlab_runner.cluster.platform_kubernetes.rbac! + end + + it { is_expected.to be_rbac } + end + context 'application failed to install previously' do let(:gitlab_runner) { create(:clusters_applications_runner, :errored, runner: ci_runner, version: '0.1.13') } diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb index 6f66515b45f..2727191eb9b 100644 --- a/spec/models/clusters/cluster_spec.rb +++ b/spec/models/clusters/cluster_spec.rb @@ -13,6 +13,10 @@ describe Clusters::Cluster do it { is_expected.to delegate_method(:status_reason).to(:provider) } it { is_expected.to delegate_method(:status_name).to(:provider) } it { is_expected.to delegate_method(:on_creation?).to(:provider) } + it { is_expected.to delegate_method(:active?).to(:platform_kubernetes).with_prefix } + it { is_expected.to delegate_method(:rbac?).to(:platform_kubernetes).with_prefix } + it { is_expected.to delegate_method(:installed?).to(:application_helm).with_prefix } + it { is_expected.to delegate_method(:installed?).to(:application_ingress).with_prefix } it { is_expected.to respond_to :project } describe '.enabled' do diff --git a/spec/models/clusters/platforms/kubernetes_spec.rb b/spec/models/clusters/platforms/kubernetes_spec.rb index ab7f89f9bf4..66198d5ee2b 100644 --- a/spec/models/clusters/platforms/kubernetes_spec.rb +++ b/spec/models/clusters/platforms/kubernetes_spec.rb @@ -92,6 +92,30 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching end end + describe '#kubeclient' do + subject { kubernetes.kubeclient } + + let(:kubernetes) { build(:cluster_platform_kubernetes, :configured, namespace: 'a-namespace') } + + it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::KubeClient) } + end + + describe '#rbac?' do + subject { kubernetes.rbac? } + + let(:kubernetes) { build(:cluster_platform_kubernetes, :configured) } + + context 'when authorization type is rbac' do + let(:kubernetes) { build(:cluster_platform_kubernetes, :rbac_enabled, :configured) } + + it { is_expected.to be_truthy } + end + + context 'when authorization type is nil' do + it { is_expected.to be_falsey } + end + end + describe '#actual_namespace' do subject { kubernetes.actual_namespace } diff --git a/spec/models/concerns/case_sensitivity_spec.rb b/spec/models/concerns/case_sensitivity_spec.rb index 5c0dfaeb4d3..1bf6c9b3404 100644 --- a/spec/models/concerns/case_sensitivity_spec.rb +++ b/spec/models/concerns/case_sensitivity_spec.rb @@ -3,186 +3,50 @@ require 'spec_helper' describe CaseSensitivity do describe '.iwhere' do let(:connection) { ActiveRecord::Base.connection } - let(:model) { Class.new { include CaseSensitivity } } - - describe 'using PostgreSQL' do - before do - allow(Gitlab::Database).to receive(:postgresql?).and_return(true) - allow(Gitlab::Database).to receive(:mysql?).and_return(false) - end - - describe 'with a single column/value pair' do - it 'returns the criteria for a column and a value' do - criteria = double(:criteria) - - expect(connection).to receive(:quote_table_name) - .with(:foo) - .and_return('"foo"') - - expect(model).to receive(:where) - .with(%q{LOWER("foo") = LOWER(:value)}, value: 'bar') - .and_return(criteria) - - expect(model.iwhere(foo: 'bar')).to eq(criteria) - end - - it 'returns the criteria for a column with a table, and a value' do - criteria = double(:criteria) - - expect(connection).to receive(:quote_table_name) - .with(:'foo.bar') - .and_return('"foo"."bar"') - - expect(model).to receive(:where) - .with(%q{LOWER("foo"."bar") = LOWER(:value)}, value: 'bar') - .and_return(criteria) - - expect(model.iwhere('foo.bar'.to_sym => 'bar')).to eq(criteria) - end - end - - describe 'with multiple column/value pairs' do - it 'returns the criteria for a column and a value' do - initial = double(:criteria) - final = double(:criteria) - - expect(connection).to receive(:quote_table_name) - .with(:foo) - .and_return('"foo"') - - expect(connection).to receive(:quote_table_name) - .with(:bar) - .and_return('"bar"') - - expect(model).to receive(:where) - .with(%q{LOWER("foo") = LOWER(:value)}, value: 'bar') - .and_return(initial) - - expect(initial).to receive(:where) - .with(%q{LOWER("bar") = LOWER(:value)}, value: 'baz') - .and_return(final) - - got = model.iwhere(foo: 'bar', bar: 'baz') - - expect(got).to eq(final) - end - - it 'returns the criteria for a column with a table, and a value' do - initial = double(:criteria) - final = double(:criteria) - - expect(connection).to receive(:quote_table_name) - .with(:'foo.bar') - .and_return('"foo"."bar"') - - expect(connection).to receive(:quote_table_name) - .with(:'foo.baz') - .and_return('"foo"."baz"') - - expect(model).to receive(:where) - .with(%q{LOWER("foo"."bar") = LOWER(:value)}, value: 'bar') - .and_return(initial) - - expect(initial).to receive(:where) - .with(%q{LOWER("foo"."baz") = LOWER(:value)}, value: 'baz') - .and_return(final) - - got = model.iwhere('foo.bar'.to_sym => 'bar', - 'foo.baz'.to_sym => 'baz') - - expect(got).to eq(final) - end + let(:model) do + Class.new(ActiveRecord::Base) do + include CaseSensitivity + self.table_name = 'namespaces' end end - describe 'using MySQL' do - before do - allow(Gitlab::Database).to receive(:postgresql?).and_return(false) - allow(Gitlab::Database).to receive(:mysql?).and_return(true) - end - - describe 'with a single column/value pair' do - it 'returns the criteria for a column and a value' do - criteria = double(:criteria) - - expect(connection).to receive(:quote_table_name) - .with(:foo) - .and_return('`foo`') - - expect(model).to receive(:where) - .with(%q{`foo` = :value}, value: 'bar') - .and_return(criteria) + let!(:model_1) { model.create(path: 'mOdEl-1', name: 'mOdEl 1') } + let!(:model_2) { model.create(path: 'mOdEl-2', name: 'mOdEl 2') } - expect(model.iwhere(foo: 'bar')).to eq(criteria) - end + it 'finds a single instance by a single attribute regardless of case' do + expect(model.iwhere(path: 'MODEL-1')).to contain_exactly(model_1) + end - it 'returns the criteria for a column with a table, and a value' do - criteria = double(:criteria) + it 'finds multiple instances by a single attribute regardless of case' do + expect(model.iwhere(path: %w(MODEL-1 model-2))).to contain_exactly(model_1, model_2) + end - expect(connection).to receive(:quote_table_name) - .with(:'foo.bar') - .and_return('`foo`.`bar`') + it 'finds instances by multiple attributes' do + expect(model.iwhere(path: %w(MODEL-1 model-2), name: 'model 1')) + .to contain_exactly(model_1) + end - expect(model).to receive(:where) - .with(%q{`foo`.`bar` = :value}, value: 'bar') - .and_return(criteria) + # Using `mysql` & `postgresql` metadata-tags here because both adapters build + # the query slightly differently + context 'for MySQL', :mysql do + it 'builds a simple query' do + query = model.iwhere(path: %w(MODEL-1 model-2), name: 'model 1').to_sql + expected_query = <<~QRY.strip + SELECT `namespaces`.* FROM `namespaces` WHERE (`namespaces`.`path` IN ('MODEL-1', 'model-2')) AND (`namespaces`.`name` = 'model 1') + QRY - expect(model.iwhere('foo.bar'.to_sym => 'bar')) - .to eq(criteria) - end + expect(query).to eq(expected_query) end + end - describe 'with multiple column/value pairs' do - it 'returns the criteria for a column and a value' do - initial = double(:criteria) - final = double(:criteria) - - expect(connection).to receive(:quote_table_name) - .with(:foo) - .and_return('`foo`') - - expect(connection).to receive(:quote_table_name) - .with(:bar) - .and_return('`bar`') - - expect(model).to receive(:where) - .with(%q{`foo` = :value}, value: 'bar') - .and_return(initial) - - expect(initial).to receive(:where) - .with(%q{`bar` = :value}, value: 'baz') - .and_return(final) - - got = model.iwhere(foo: 'bar', bar: 'baz') - - expect(got).to eq(final) - end - - it 'returns the criteria for a column with a table, and a value' do - initial = double(:criteria) - final = double(:criteria) - - expect(connection).to receive(:quote_table_name) - .with(:'foo.bar') - .and_return('`foo`.`bar`') - - expect(connection).to receive(:quote_table_name) - .with(:'foo.baz') - .and_return('`foo`.`baz`') - - expect(model).to receive(:where) - .with(%q{`foo`.`bar` = :value}, value: 'bar') - .and_return(initial) - - expect(initial).to receive(:where) - .with(%q{`foo`.`baz` = :value}, value: 'baz') - .and_return(final) - - got = model.iwhere('foo.bar'.to_sym => 'bar', - 'foo.baz'.to_sym => 'baz') + context 'for PostgreSQL', :postgresql do + it 'builds a query using LOWER' do + query = model.iwhere(path: %w(MODEL-1 model-2), name: 'model 1').to_sql + expected_query = <<~QRY.strip + SELECT \"namespaces\".* FROM \"namespaces\" WHERE (LOWER(\"namespaces\".\"path\") IN (LOWER('MODEL-1'), LOWER('model-2'))) AND (LOWER(\"namespaces\".\"name\") = LOWER('model 1')) + QRY - expect(got).to eq(final) - end + expect(query).to eq(expected_query) end end end diff --git a/spec/models/concerns/triggerable_hooks_spec.rb b/spec/models/concerns/triggerable_hooks_spec.rb index 621d2d38eae..265abd6bd72 100644 --- a/spec/models/concerns/triggerable_hooks_spec.rb +++ b/spec/models/concerns/triggerable_hooks_spec.rb @@ -40,4 +40,28 @@ RSpec.describe TriggerableHooks do end end end + + describe '.select_active' do + it 'returns hooks that match the active filter' do + TestableHook.create!(url: 'http://example1.com', push_events: true) + TestableHook.create!(url: 'http://example2.com', push_events: true) + filter1 = double(:filter1) + filter2 = double(:filter2) + allow(ActiveHookFilter).to receive(:new).exactly(2).times.and_return(filter1, filter2) + expect(filter1).to receive(:matches?).and_return(true) + expect(filter2).to receive(:matches?).and_return(false) + + hooks = TestableHook.push_hooks.order_id_asc + expect(hooks.select_active(:push_hooks, {})).to eq [hooks.first] + end + + it 'returns empty list if no hooks match the active filter' do + TestableHook.create!(url: 'http://example1.com', push_events: true) + filter = double(:filter) + allow(ActiveHookFilter).to receive(:new).and_return(filter) + expect(filter).to receive(:matches?).and_return(false) + + expect(TestableHook.push_hooks.select_active(:push_hooks, {})).to eq [] + end + end end diff --git a/spec/models/hooks/active_hook_filter_spec.rb b/spec/models/hooks/active_hook_filter_spec.rb new file mode 100644 index 00000000000..df7edda2213 --- /dev/null +++ b/spec/models/hooks/active_hook_filter_spec.rb @@ -0,0 +1,68 @@ +require 'spec_helper' + +describe ActiveHookFilter do + subject(:filter) { described_class.new(hook) } + + describe '#matches?' do + context 'for push event hooks' do + let(:hook) do + create(:project_hook, push_events: true, push_events_branch_filter: branch_filter) + end + + context 'branch filter is specified' do + let(:branch_filter) { 'master' } + + it 'returns true if branch matches' do + expect(filter.matches?(:push_hooks, { ref: 'refs/heads/master' })).to be true + end + + it 'returns false if branch does not match' do + expect(filter.matches?(:push_hooks, { ref: 'refs/heads/my_branch' })).to be false + end + + it 'returns false if ref is nil' do + expect(filter.matches?(:push_hooks, {})).to be false + end + + context 'branch filter contains wildcard' do + let(:branch_filter) { 'features/*' } + + it 'returns true if branch matches' do + expect(filter.matches?(:push_hooks, { ref: 'refs/heads/features/my-branch' })).to be true + expect(filter.matches?(:push_hooks, { ref: 'refs/heads/features/my-branch/something' })).to be true + end + + it 'returns false if branch does not match' do + expect(filter.matches?(:push_hooks, { ref: 'refs/heads/master' })).to be false + end + end + end + + context 'branch filter is not specified' do + let(:branch_filter) { nil } + + it 'returns true' do + expect(filter.matches?(:push_hooks, { ref: 'refs/heads/master' })).to be true + end + end + + context 'branch filter is empty string' do + let(:branch_filter) { '' } + + it 'acts like branch is not specified' do + expect(filter.matches?(:push_hooks, { ref: 'refs/heads/master' })).to be true + end + end + end + + context 'for non-push-events hooks' do + let(:hook) do + create(:project_hook, issues_events: true, push_events: false, push_events_branch_filter: '') + end + + it 'returns true as branch filters are not yet supported for these' do + expect(filter.matches?(:issues_events, { ref: 'refs/heads/master' })).to be true + end + end + end +end diff --git a/spec/models/hooks/web_hook_spec.rb b/spec/models/hooks/web_hook_spec.rb index ea6d6e53ef5..a4181631f01 100644 --- a/spec/models/hooks/web_hook_spec.rb +++ b/spec/models/hooks/web_hook_spec.rb @@ -35,6 +35,26 @@ describe WebHook do it { is_expected.not_to allow_values("foo\nbar", "foo\r\nbar").for(:token) } end + + describe 'push_events_branch_filter' do + it { is_expected.to allow_values("good_branch_name", "another/good-branch_name").for(:push_events_branch_filter) } + it { is_expected.to allow_values("").for(:push_events_branch_filter) } + it { is_expected.not_to allow_values("bad branch name", "bad~branchname").for(:push_events_branch_filter) } + + it 'gets rid of whitespace' do + hook.push_events_branch_filter = ' branch ' + hook.save + + expect(hook.push_events_branch_filter).to eq('branch') + end + + it 'stores whitespace only as empty' do + hook.push_events_branch_filter = ' ' + hook.save + + expect(hook.push_events_branch_filter).to eq('') + end + end end describe 'execute' do diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 7cfffbde42f..264632dba4b 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -3734,21 +3734,45 @@ describe Project do end describe '#execute_hooks' do - it 'executes the projects hooks with the specified scope' do - hook1 = create(:project_hook, merge_requests_events: true, tag_push_events: false) - hook2 = create(:project_hook, merge_requests_events: false, tag_push_events: true) - project = create(:project, hooks: [hook1, hook2]) + let(:data) { { ref: 'refs/heads/master', data: 'data' } } + it 'executes active projects hooks with the specified scope' do + hook = create(:project_hook, merge_requests_events: false, push_events: true) + expect(ProjectHook).to receive(:select_active) + .with(:push_hooks, data) + .and_return([hook]) + project = create(:project, hooks: [hook]) expect_any_instance_of(ProjectHook).to receive(:async_execute).once - project.execute_hooks({}, :tag_push_hooks) + project.execute_hooks(data, :push_hooks) + end + + it 'does not execute project hooks that dont match the specified scope' do + hook = create(:project_hook, merge_requests_events: true, push_events: false) + project = create(:project, hooks: [hook]) + + expect_any_instance_of(ProjectHook).not_to receive(:async_execute).once + + project.execute_hooks(data, :push_hooks) + end + + it 'does not execute project hooks which are not active' do + hook = create(:project_hook, push_events: true) + expect(ProjectHook).to receive(:select_active) + .with(:push_hooks, data) + .and_return([]) + project = create(:project, hooks: [hook]) + + expect_any_instance_of(ProjectHook).not_to receive(:async_execute).once + + project.execute_hooks(data, :push_hooks) end it 'executes the system hooks with the specified scope' do - expect_any_instance_of(SystemHooksService).to receive(:execute_hooks).with({ data: 'data' }, :merge_request_hooks) + expect_any_instance_of(SystemHooksService).to receive(:execute_hooks).with(data, :merge_request_hooks) project = build(:project) - project.execute_hooks({ data: 'data' }, :merge_request_hooks) + project.execute_hooks(data, :merge_request_hooks) end it 'executes the system hooks when inside a transaction' do @@ -3763,7 +3787,7 @@ describe Project do # actually get to the `after_commit` hook that queues these jobs. expect do project.transaction do - project.execute_hooks({ data: 'data' }, :merge_request_hooks) + project.execute_hooks(data, :merge_request_hooks) end end.not_to raise_error # Sidekiq::Worker::EnqueueFromTransactionError end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index fd99acb3bb2..2a7aff39240 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -405,6 +405,23 @@ describe User do end end end + + describe '.by_username' do + it 'finds users regardless of the case passed' do + user = create(:user, username: 'CaMeLcAsEd') + user2 = create(:user, username: 'UPPERCASE') + + expect(described_class.by_username(%w(CAMELCASED uppercase))) + .to contain_exactly(user, user2) + end + + it 'finds a single user regardless of the case passed' do + user = create(:user, username: 'CaMeLcAsEd') + + expect(described_class.by_username('CAMELCASED')) + .to contain_exactly(user) + end + end end describe "Respond to" do diff --git a/spec/presenters/project_presenter_spec.rb b/spec/presenters/project_presenter_spec.rb index 01085dbcb49..d9fb27e101e 100644 --- a/spec/presenters/project_presenter_spec.rb +++ b/spec/presenters/project_presenter_spec.rb @@ -159,39 +159,76 @@ describe ProjectPresenter do end end + context 'statistics anchors (empty repo)' do + let(:project) { create(:project, :empty_repo) } + let(:presenter) { described_class.new(project, current_user: user) } + + describe '#files_anchor_data' do + it 'returns files data' do + expect(presenter.files_anchor_data).to have_attributes(enabled: true, + label: 'Files (0 Bytes)', + link: nil) + end + end + + describe '#commits_anchor_data' do + it 'returns commits data' do + expect(presenter.commits_anchor_data).to have_attributes(enabled: true, + label: 'Commits (0)', + link: nil) + end + end + + describe '#branches_anchor_data' do + it 'returns branches data' do + expect(presenter.branches_anchor_data).to have_attributes(enabled: true, + label: "Branches (0)", + link: nil) + end + end + + describe '#tags_anchor_data' do + it 'returns tags data' do + expect(presenter.tags_anchor_data).to have_attributes(enabled: true, + label: "Tags (0)", + link: nil) + end + end + end + context 'statistics anchors' do let(:project) { create(:project, :repository) } let(:presenter) { described_class.new(project, current_user: user) } describe '#files_anchor_data' do it 'returns files data' do - expect(presenter.files_anchor_data).to eq(OpenStruct.new(enabled: true, - label: 'Files (0 Bytes)', - link: presenter.project_tree_path(project))) + expect(presenter.files_anchor_data).to have_attributes(enabled: true, + label: 'Files (0 Bytes)', + link: presenter.project_tree_path(project)) end end describe '#commits_anchor_data' do it 'returns commits data' do - expect(presenter.commits_anchor_data).to eq(OpenStruct.new(enabled: true, - label: 'Commits (0)', - link: presenter.project_commits_path(project, project.repository.root_ref))) + expect(presenter.commits_anchor_data).to have_attributes(enabled: true, + label: 'Commits (0)', + link: presenter.project_commits_path(project, project.repository.root_ref)) end end describe '#branches_anchor_data' do it 'returns branches data' do - expect(presenter.branches_anchor_data).to eq(OpenStruct.new(enabled: true, - label: "Branches (#{project.repository.branches.size})", - link: presenter.project_branches_path(project))) + expect(presenter.branches_anchor_data).to have_attributes(enabled: true, + label: "Branches (#{project.repository.branches.size})", + link: presenter.project_branches_path(project)) end end describe '#tags_anchor_data' do it 'returns tags data' do - expect(presenter.tags_anchor_data).to eq(OpenStruct.new(enabled: true, - label: "Tags (#{project.repository.tags.size})", - link: presenter.project_tags_path(project))) + expect(presenter.tags_anchor_data).to have_attributes(enabled: true, + label: "Tags (#{project.repository.tags.size})", + link: presenter.project_tags_path(project)) end end @@ -199,10 +236,10 @@ describe ProjectPresenter do it 'returns new file data if user can push' do project.add_developer(user) - expect(presenter.new_file_anchor_data).to eq(OpenStruct.new(enabled: false, - label: "New file", - link: presenter.project_new_blob_path(project, 'master'), - class_modifier: 'new')) + expect(presenter.new_file_anchor_data).to have_attributes(enabled: false, + label: "New file", + link: presenter.project_new_blob_path(project, 'master'), + class_modifier: 'new') end it 'returns nil if user cannot push' do @@ -227,9 +264,9 @@ describe ProjectPresenter do project.add_developer(user) allow(project.repository).to receive(:readme).and_return(nil) - expect(presenter.readme_anchor_data).to eq(OpenStruct.new(enabled: false, - label: 'Add Readme', - link: presenter.add_readme_path)) + expect(presenter.readme_anchor_data).to have_attributes(enabled: false, + label: 'Add Readme', + link: presenter.add_readme_path) end end @@ -237,9 +274,9 @@ describe ProjectPresenter do it 'returns anchor data' do allow(project.repository).to receive(:readme).and_return(double(name: 'readme')) - expect(presenter.readme_anchor_data).to eq(OpenStruct.new(enabled: true, - label: 'Readme', - link: presenter.readme_path)) + expect(presenter.readme_anchor_data).to have_attributes(enabled: true, + label: 'Readme', + link: presenter.readme_path) end end end @@ -250,9 +287,9 @@ describe ProjectPresenter do project.add_developer(user) allow(project.repository).to receive(:changelog).and_return(nil) - expect(presenter.changelog_anchor_data).to eq(OpenStruct.new(enabled: false, - label: 'Add Changelog', - link: presenter.add_changelog_path)) + expect(presenter.changelog_anchor_data).to have_attributes(enabled: false, + label: 'Add Changelog', + link: presenter.add_changelog_path) end end @@ -260,9 +297,9 @@ describe ProjectPresenter do it 'returns anchor data' do allow(project.repository).to receive(:changelog).and_return(double(name: 'foo')) - expect(presenter.changelog_anchor_data).to eq(OpenStruct.new(enabled: true, - label: 'Changelog', - link: presenter.changelog_path)) + expect(presenter.changelog_anchor_data).to have_attributes(enabled: true, + label: 'Changelog', + link: presenter.changelog_path) end end end @@ -273,9 +310,9 @@ describe ProjectPresenter do project.add_developer(user) allow(project.repository).to receive(:license_blob).and_return(nil) - expect(presenter.license_anchor_data).to eq(OpenStruct.new(enabled: false, - label: 'Add License', - link: presenter.add_license_path)) + expect(presenter.license_anchor_data).to have_attributes(enabled: false, + label: 'Add license', + link: presenter.add_license_path) end end @@ -283,9 +320,9 @@ describe ProjectPresenter do it 'returns anchor data' do allow(project.repository).to receive(:license_blob).and_return(double(name: 'foo')) - expect(presenter.license_anchor_data).to eq(OpenStruct.new(enabled: true, - label: presenter.license_short_name, - link: presenter.license_path)) + expect(presenter.license_anchor_data).to have_attributes(enabled: true, + label: presenter.license_short_name, + link: presenter.license_path) end end end @@ -296,9 +333,9 @@ describe ProjectPresenter do project.add_developer(user) allow(project.repository).to receive(:contribution_guide).and_return(nil) - expect(presenter.contribution_guide_anchor_data).to eq(OpenStruct.new(enabled: false, - label: 'Add Contribution guide', - link: presenter.add_contribution_guide_path)) + expect(presenter.contribution_guide_anchor_data).to have_attributes(enabled: false, + label: 'Add Contribution guide', + link: presenter.add_contribution_guide_path) end end @@ -306,9 +343,9 @@ describe ProjectPresenter do it 'returns anchor data' do allow(project.repository).to receive(:contribution_guide).and_return(double(name: 'foo')) - expect(presenter.contribution_guide_anchor_data).to eq(OpenStruct.new(enabled: true, - label: 'Contribution guide', - link: presenter.contribution_guide_path)) + expect(presenter.contribution_guide_anchor_data).to have_attributes(enabled: true, + label: 'Contribution guide', + link: presenter.contribution_guide_path) end end end @@ -318,9 +355,9 @@ describe ProjectPresenter do it 'returns anchor data' do allow(project).to receive(:auto_devops_enabled?).and_return(true) - expect(presenter.autodevops_anchor_data).to eq(OpenStruct.new(enabled: true, - label: 'Auto DevOps enabled', - link: nil)) + expect(presenter.autodevops_anchor_data).to have_attributes(enabled: true, + label: 'Auto DevOps enabled', + link: nil) end end @@ -330,9 +367,9 @@ describe ProjectPresenter do allow(project).to receive(:auto_devops_enabled?).and_return(false) allow(project.repository).to receive(:gitlab_ci_yml).and_return(nil) - expect(presenter.autodevops_anchor_data).to eq(OpenStruct.new(enabled: false, - label: 'Enable Auto DevOps', - link: presenter.project_settings_ci_cd_path(project, anchor: 'autodevops-settings'))) + expect(presenter.autodevops_anchor_data).to have_attributes(enabled: false, + label: 'Enable Auto DevOps', + link: presenter.project_settings_ci_cd_path(project, anchor: 'autodevops-settings')) end end end @@ -343,9 +380,9 @@ describe ProjectPresenter do project.add_maintainer(user) cluster = create(:cluster, projects: [project]) - expect(presenter.kubernetes_cluster_anchor_data).to eq(OpenStruct.new(enabled: true, - label: 'Kubernetes configured', - link: presenter.project_cluster_path(project, cluster))) + expect(presenter.kubernetes_cluster_anchor_data).to have_attributes(enabled: true, + label: 'Kubernetes configured', + link: presenter.project_cluster_path(project, cluster)) end it 'returns link to clusters page if more than one exists' do @@ -353,17 +390,17 @@ describe ProjectPresenter do create(:cluster, :production_environment, projects: [project]) create(:cluster, projects: [project]) - expect(presenter.kubernetes_cluster_anchor_data).to eq(OpenStruct.new(enabled: true, - label: 'Kubernetes configured', - link: presenter.project_clusters_path(project))) + expect(presenter.kubernetes_cluster_anchor_data).to have_attributes(enabled: true, + label: 'Kubernetes configured', + link: presenter.project_clusters_path(project)) end it 'returns link to create a cluster if no cluster exists' do project.add_maintainer(user) - expect(presenter.kubernetes_cluster_anchor_data).to eq(OpenStruct.new(enabled: false, - label: 'Add Kubernetes cluster', - link: presenter.new_project_cluster_path(project))) + expect(presenter.kubernetes_cluster_anchor_data).to have_attributes(enabled: false, + label: 'Add Kubernetes cluster', + link: presenter.new_project_cluster_path(project)) end end @@ -380,9 +417,9 @@ describe ProjectPresenter do allow(project.repository).to receive(:koding_yml).and_return(nil) allow(Gitlab::CurrentSettings).to receive(:koding_enabled?).and_return(true) - expect(presenter.koding_anchor_data).to eq(OpenStruct.new(enabled: false, - label: 'Set up Koding', - link: presenter.add_koding_stack_path)) + expect(presenter.koding_anchor_data).to have_attributes(enabled: false, + label: 'Set up Koding', + link: presenter.add_koding_stack_path) end it 'returns nil if user cannot push' do diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb index 85c93f35c20..6890f46c724 100644 --- a/spec/requests/api/internal_spec.rb +++ b/spec/requests/api/internal_spec.rb @@ -381,7 +381,7 @@ describe API::Internal do it do pull(key, project) - expect(response).to have_gitlab_http_status(200) + expect(response).to have_gitlab_http_status(401) expect(json_response["status"]).to be_falsey expect(user.reload.last_activity_on).to be_nil end @@ -391,13 +391,61 @@ describe API::Internal do it do push(key, project) - expect(response).to have_gitlab_http_status(200) + expect(response).to have_gitlab_http_status(401) expect(json_response["status"]).to be_falsey expect(user.reload.last_activity_on).to be_nil end end end + context "custom action" do + let(:access_checker) { double(Gitlab::GitAccess) } + let(:message) { 'CustomActionError message' } + let(:payload) do + { + 'action' => 'geo_proxy_to_primary', + 'data' => { + 'api_endpoints' => %w{geo/proxy_git_push_ssh/info_refs geo/proxy_git_push_ssh/push}, + 'gl_username' => 'testuser', + 'primary_repo' => 'http://localhost:3000/testuser/repo.git' + } + } + end + + let(:custom_action_result) { Gitlab::GitAccessResult::CustomAction.new(payload, message) } + + before do + project.add_guest(user) + expect(Gitlab::GitAccess).to receive(:new).with( + key, + project, + 'ssh', + { + authentication_abilities: [:read_project, :download_code, :push_code], + namespace_path: project.namespace.name, + project_path: project.path, + redirected_path: nil + } + ).and_return(access_checker) + expect(access_checker).to receive(:check).with( + 'git-receive-pack', + 'd14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/master' + ).and_return(custom_action_result) + end + + context "git push" do + it do + push(key, project) + + expect(response).to have_gitlab_http_status(300) + expect(json_response['status']).to be_truthy + expect(json_response['message']).to eql(message) + expect(json_response['payload']).to eql(payload) + expect(user.reload.last_activity_on).to be_nil + end + end + end + context "blocked user" do let(:personal_project) { create(:project, namespace: user.namespace) } @@ -409,7 +457,7 @@ describe API::Internal do it do pull(key, personal_project) - expect(response).to have_gitlab_http_status(200) + expect(response).to have_gitlab_http_status(401) expect(json_response["status"]).to be_falsey expect(user.reload.last_activity_on).to be_nil end @@ -419,7 +467,7 @@ describe API::Internal do it do push(key, personal_project) - expect(response).to have_gitlab_http_status(200) + expect(response).to have_gitlab_http_status(401) expect(json_response["status"]).to be_falsey expect(user.reload.last_activity_on).to be_nil end @@ -445,7 +493,7 @@ describe API::Internal do it do push(key, project) - expect(response).to have_gitlab_http_status(200) + expect(response).to have_gitlab_http_status(401) expect(json_response["status"]).to be_falsey end end @@ -477,7 +525,7 @@ describe API::Internal do it do archive(key, project) - expect(response).to have_gitlab_http_status(200) + expect(response).to have_gitlab_http_status(404) expect(json_response["status"]).to be_falsey end end @@ -489,7 +537,7 @@ describe API::Internal do pull(key, project) - expect(response).to have_gitlab_http_status(200) + expect(response).to have_gitlab_http_status(404) expect(json_response["status"]).to be_falsey end end @@ -498,7 +546,7 @@ describe API::Internal do it do pull(OpenStruct.new(id: 0), project) - expect(response).to have_gitlab_http_status(200) + expect(response).to have_gitlab_http_status(404) expect(json_response["status"]).to be_falsey end end @@ -511,7 +559,7 @@ describe API::Internal do it 'rejects the SSH push' do push(key, project) - expect(response.status).to eq(200) + expect(response.status).to eq(401) expect(json_response['status']).to be_falsey expect(json_response['message']).to eq 'Git access over SSH is not allowed' end @@ -519,7 +567,7 @@ describe API::Internal do it 'rejects the SSH pull' do pull(key, project) - expect(response.status).to eq(200) + expect(response.status).to eq(401) expect(json_response['status']).to be_falsey expect(json_response['message']).to eq 'Git access over SSH is not allowed' end @@ -533,7 +581,7 @@ describe API::Internal do it 'rejects the HTTP push' do push(key, project, 'http') - expect(response.status).to eq(200) + expect(response.status).to eq(401) expect(json_response['status']).to be_falsey expect(json_response['message']).to eq 'Git access over HTTP is not allowed' end @@ -541,7 +589,7 @@ describe API::Internal do it 'rejects the HTTP pull' do pull(key, project, 'http') - expect(response.status).to eq(200) + expect(response.status).to eq(401) expect(json_response['status']).to be_falsey expect(json_response['message']).to eq 'Git access over HTTP is not allowed' end @@ -571,14 +619,14 @@ describe API::Internal do it 'rejects the push' do push(key, project) - expect(response).to have_gitlab_http_status(200) + expect(response).to have_gitlab_http_status(404) expect(json_response['status']).to be_falsy end it 'rejects the SSH pull' do pull(key, project) - expect(response).to have_gitlab_http_status(200) + expect(response).to have_gitlab_http_status(404) expect(json_response['status']).to be_falsy end end diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 4de834bf93a..e987eee6e91 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -353,6 +353,15 @@ describe API::MergeRequests do end end + it 'returns the commits behind the target branch when include_diverged_commits_count is present' do + allow_any_instance_of(merge_request.class).to receive(:diverged_commits_count).and_return(1) + + get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), include_diverged_commits_count: true + + expect(response).to have_gitlab_http_status(200) + expect(json_response['diverged_commits_count']).to eq(1) + end + it "returns a 404 error if merge_request_iid not found" do get api("/projects/#{project.id}/merge_requests/999", user) expect(response).to have_gitlab_http_status(404) diff --git a/spec/requests/api/project_hooks_spec.rb b/spec/requests/api/project_hooks_spec.rb index d3f81cc038d..87997a48dc9 100644 --- a/spec/requests/api/project_hooks_spec.rb +++ b/spec/requests/api/project_hooks_spec.rb @@ -9,7 +9,8 @@ describe API::ProjectHooks, 'ProjectHooks' do :all_events_enabled, project: project, url: 'http://example.com', - enable_ssl_verification: true) + enable_ssl_verification: true, + push_events_branch_filter: 'master') end before do @@ -38,6 +39,7 @@ describe API::ProjectHooks, 'ProjectHooks' do expect(json_response.first['pipeline_events']).to eq(true) expect(json_response.first['wiki_page_events']).to eq(true) expect(json_response.first['enable_ssl_verification']).to eq(true) + expect(json_response.first['push_events_branch_filter']).to eq('master') end end @@ -90,7 +92,7 @@ describe API::ProjectHooks, 'ProjectHooks' do expect do post api("/projects/#{project.id}/hooks", user), url: "http://example.com", issues_events: true, confidential_issues_events: true, wiki_page_events: true, - job_events: true + job_events: true, push_events_branch_filter: 'some-feature-branch' end.to change {project.hooks.count}.by(1) expect(response).to have_gitlab_http_status(201) @@ -106,6 +108,7 @@ describe API::ProjectHooks, 'ProjectHooks' do expect(json_response['pipeline_events']).to eq(false) expect(json_response['wiki_page_events']).to eq(true) expect(json_response['enable_ssl_verification']).to eq(true) + expect(json_response['push_events_branch_filter']).to eq('some-feature-branch') expect(json_response).not_to include('token') end @@ -132,7 +135,12 @@ describe API::ProjectHooks, 'ProjectHooks' do end it "returns a 422 error if url not valid" do - post api("/projects/#{project.id}/hooks", user), "url" => "ftp://example.com" + post api("/projects/#{project.id}/hooks", user), url: "ftp://example.com" + expect(response).to have_gitlab_http_status(422) + end + + it "returns a 422 error if branch filter is not valid" do + post api("/projects/#{project.id}/hooks", user), url: "http://example.com", push_events_branch_filter: '~badbranchname/' expect(response).to have_gitlab_http_status(422) end end diff --git a/spec/rubocop/cop/line_break_around_conditional_block_spec.rb b/spec/rubocop/cop/line_break_around_conditional_block_spec.rb index 03eeffe6483..892b393c307 100644 --- a/spec/rubocop/cop/line_break_around_conditional_block_spec.rb +++ b/spec/rubocop/cop/line_break_around_conditional_block_spec.rb @@ -328,6 +328,22 @@ describe RuboCop::Cop::LineBreakAroundConditionalBlock do expect(cop.offenses).to be_empty end + it "doesn't flag violation for #{conditional} preceded by a rescue" do + source = <<~RUBY + def a_method + do_something + rescue + #{conditional} condition + do_something + end + end + RUBY + + inspect_source(source) + + expect(cop.offenses).to be_empty + end + it "doesn't flag violation for #{conditional} followed by a rescue" do source = <<~RUBY def a_method diff --git a/spec/services/preview_markdown_service_spec.rb b/spec/services/preview_markdown_service_spec.rb index 507909d9231..b69977c812a 100644 --- a/spec/services/preview_markdown_service_spec.rb +++ b/spec/services/preview_markdown_service_spec.rb @@ -101,4 +101,11 @@ describe PreviewMarkdownService do expect(result[:markdown_engine]).to eq :common_mark end + + it 'honors the legacy_render parameter' do + service = described_class.new(project, user, { legacy_render: '1' }) + result = service.execute + + expect(result[:markdown_engine]).to eq :redcarpet + end end diff --git a/spec/support/helpers/kubernetes_helpers.rb b/spec/support/helpers/kubernetes_helpers.rb index 683a64504a1..994a2aaef90 100644 --- a/spec/support/helpers/kubernetes_helpers.rb +++ b/spec/support/helpers/kubernetes_helpers.rb @@ -16,6 +16,7 @@ module KubernetesHelpers def stub_kubeclient_discover(api_url) WebMock.stub_request(:get, api_url + '/api/v1').to_return(kube_response(kube_v1_discovery_body)) WebMock.stub_request(:get, api_url + '/apis/extensions/v1beta1').to_return(kube_response(kube_v1beta1_discovery_body)) + WebMock.stub_request(:get, api_url + '/apis/rbac.authorization.k8s.io/v1').to_return(kube_response(kube_v1_rbac_authorization_discovery_body)) end def stub_kubeclient_pods(response = nil) @@ -66,7 +67,8 @@ module KubernetesHelpers "resources" => [ { "name" => "pods", "namespaced" => true, "kind" => "Pod" }, { "name" => "deployments", "namespaced" => true, "kind" => "Deployment" }, - { "name" => "secrets", "namespaced" => true, "kind" => "Secret" } + { "name" => "secrets", "namespaced" => true, "kind" => "Secret" }, + { "name" => "services", "namespaced" => true, "kind" => "Service" } ] } end @@ -77,7 +79,20 @@ module KubernetesHelpers "resources" => [ { "name" => "pods", "namespaced" => true, "kind" => "Pod" }, { "name" => "deployments", "namespaced" => true, "kind" => "Deployment" }, - { "name" => "secrets", "namespaced" => true, "kind" => "Secret" } + { "name" => "secrets", "namespaced" => true, "kind" => "Secret" }, + { "name" => "services", "namespaced" => true, "kind" => "Service" } + ] + } + end + + def kube_v1_rbac_authorization_discovery_body + { + "kind" => "APIResourceList", + "resources" => [ + { "name" => "clusterrolebindings", "namespaced" => false, "kind" => "ClusterRoleBinding" }, + { "name" => "clusterroles", "namespaced" => false, "kind" => "ClusterRole" }, + { "name" => "rolebindings", "namespaced" => true, "kind" => "RoleBinding" }, + { "name" => "roles", "namespaced" => true, "kind" => "Role" } ] } end diff --git a/spec/support/helpers/stub_feature_flags.rb b/spec/support/helpers/stub_feature_flags.rb index c54a871b157..4061a8d1bc9 100644 --- a/spec/support/helpers/stub_feature_flags.rb +++ b/spec/support/helpers/stub_feature_flags.rb @@ -4,8 +4,8 @@ module StubFeatureFlags # @param [Hash] features where key is feature name and value is boolean whether enabled or not def stub_feature_flags(features) features.each do |feature_name, enabled| - allow(Feature).to receive(:enabled?).with(feature_name) { enabled } - allow(Feature).to receive(:enabled?).with(feature_name.to_s) { enabled } + allow(Feature).to receive(:enabled?).with(feature_name, any_args) { enabled } + allow(Feature).to receive(:enabled?).with(feature_name.to_s, any_args) { enabled } end end end diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb index 21103771d1f..3f8e3ae5190 100644 --- a/spec/support/helpers/test_env.rb +++ b/spec/support/helpers/test_env.rb @@ -52,7 +52,8 @@ module TestEnv 'add_images_and_changes' => '010d106', 'update-gitlab-shell-v-6-0-1' => '2f61d70', 'update-gitlab-shell-v-6-0-3' => 'de78448', - '2-mb-file' => 'bf12d25' + '2-mb-file' => 'bf12d25', + 'with-codeowners' => '219560e' }.freeze # gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily diff --git a/spec/validators/branch_filter_validator_spec.rb b/spec/validators/branch_filter_validator_spec.rb new file mode 100644 index 00000000000..3be54827431 --- /dev/null +++ b/spec/validators/branch_filter_validator_spec.rb @@ -0,0 +1,42 @@ +require 'spec_helper' + +describe BranchFilterValidator do + let(:validator) { described_class.new(attributes: [:push_events_branch_filter]) } + let(:hook) { build(:project_hook) } + + describe '#validates_each' do + it 'allows valid branch names' do + validator.validate_each(hook, :push_events_branch_filter, "good_branch_name") + validator.validate_each(hook, :push_events_branch_filter, "another/good_branch_name") + expect(hook.errors.empty?).to be true + end + + it 'disallows bad branch names' do + validator.validate_each(hook, :push_events_branch_filter, "bad branch~name") + expect(hook.errors[:push_events_branch_filter].empty?).to be false + end + + it 'allows wildcards' do + validator.validate_each(hook, :push_events_branch_filter, "features/*") + validator.validate_each(hook, :push_events_branch_filter, "features/*/bla") + validator.validate_each(hook, :push_events_branch_filter, "*-stable") + expect(hook.errors.empty?).to be true + end + + it 'gets rid of whitespace' do + filter = ' master ' + validator.validate_each(hook, :push_events_branch_filter, filter) + + expect(filter).to eq 'master' + end + + # Branch names can be quite long but in practice aren't over 255 so 4000 should + # be enough space for a list of branch names but we can increase if needed. + it 'limits length to 4000 chars' do + filter = 'a' * 4001 + validator.validate_each(hook, :push_events_branch_filter, filter) + + expect(hook.errors[:push_events_branch_filter].empty?).to be false + end + end +end diff --git a/spec/views/projects/_home_panel.html.haml_spec.rb b/spec/views/projects/_home_panel.html.haml_spec.rb index b56940a9613..fc1fe5739c3 100644 --- a/spec/views/projects/_home_panel.html.haml_spec.rb +++ b/spec/views/projects/_home_panel.html.haml_spec.rb @@ -9,6 +9,7 @@ describe 'projects/_home_panel' do allow(view).to receive(:current_user).and_return(user) allow(view).to receive(:can?).with(user, :read_project, project).and_return(false) + allow(project).to receive(:license_anchor_data).and_return(false) end context 'when user is signed in' do @@ -63,6 +64,7 @@ describe 'projects/_home_panel' do allow(view).to receive(:current_user).and_return(user) allow(view).to receive(:can?).with(user, :read_project, project).and_return(false) + allow(project).to receive(:license_anchor_data).and_return(false) end context 'has no badges' do @@ -71,8 +73,7 @@ describe 'projects/_home_panel' do it 'should not render any badge' do render - expect(rendered).to have_selector('.project-badges') - expect(rendered).not_to have_selector('.project-badges > a') + expect(rendered).not_to have_selector('.project-badges') end end @@ -118,6 +119,7 @@ describe 'projects/_home_panel' do assign(:project, project) allow(view).to receive(:current_user).and_return(user) + allow(project).to receive(:license_anchor_data).and_return(false) end context 'user can read project' do diff --git a/yarn.lock b/yarn.lock index db3b0bbe573..6f96e5ff228 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5274,13 +5274,13 @@ moment@2.x, moment@^2.18.1: version "2.19.2" resolved "https://registry.yarnpkg.com/moment/-/moment-2.19.2.tgz#8a7f774c95a64550b4c7ebd496683908f9419dbe" -monaco-editor-webpack-plugin@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/monaco-editor-webpack-plugin/-/monaco-editor-webpack-plugin-1.4.0.tgz#7324258ab3695464cfe3bc12edb2e8c55b80d92f" +monaco-editor-webpack-plugin@^1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/monaco-editor-webpack-plugin/-/monaco-editor-webpack-plugin-1.5.2.tgz#e113fa1d5759ede6fd776eb620cdd5930203b55a" -monaco-editor@0.13.1: - version "0.13.1" - resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.13.1.tgz#6b9ce20e4d1c945042d256825eb133cb23315a52" +monaco-editor@^0.14.3: + version "0.14.3" + resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.14.3.tgz#7cc4a4096a3821f52fea9b10489b527ef3034e22" mousetrap@^1.4.6: version "1.4.6" |