diff options
288 files changed, 4420 insertions, 1599 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 6c3862c7645..ad5c194bffe 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1039,15 +1039,7 @@ schedule:review-build-cng: - source ./scripts/review_apps/review-apps.sh script: - wait_for_job_to_be_done "review-build-cng" - after_script: - - source ./scripts/review_apps/review-apps.sh - - check_kube_domain - - download_gitlab_chart - - ensure_namespace - - install_tiller - - install_external_dns - - time deploy - - add_license + - perform_review_app_deployment review-deploy: <<: *review-deploy-base @@ -1057,6 +1049,7 @@ schedule:review-deploy: <<: *review-schedules-only script: - wait_for_job_to_be_done "schedule:review-build-cng" + - perform_review_app_deployment .review-qa-base: &review-qa-base <<: *review-docker @@ -1103,8 +1096,7 @@ review-qa-all: <<: *review-qa-base script: - wait_for_job_to_be_done "review-deploy" - after_script: - - mkdir gitlab-exporter + - mkdir -p gitlab-exporter - wget -O ./gitlab-exporter/index.js https://gitlab.com/gitlab-org/gl-performance/raw/master/index.js - mkdir sitespeed-results - docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io sitespeedio/sitespeed.io:6.3.1 --plugins.add ./gitlab-exporter --outputFolder sitespeed-results "$CI_ENVIRONMENT_URL" diff --git a/.gitlab/issue_templates/Test plan.md b/.gitlab/issue_templates/Test plan.md index a3c3f4a6509..3aedd5859d3 100644 --- a/.gitlab/issue_templates/Test plan.md +++ b/.gitlab/issue_templates/Test plan.md @@ -93,4 +93,4 @@ When adding new automated tests, please keep [testing levels](https://docs.gitla in mind. --> -/label ~Quality ~"test plan" +/label ~Quality ~"test\-plan" diff --git a/CHANGELOG.md b/CHANGELOG.md index bec6a8b3e21..0c33596d9c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 11.8.3 (2019-03-19) + +### Security (1 change) + +- Remove project serialization in quick actions response. + + ## 11.8.2 (2019-03-13) ### Security (1 change) @@ -264,6 +271,14 @@ entry. - Creates mixin to reduce code duplication between CE and EE in graph component. +## 11.7.7 (2019-03-19) + +### Security (2 changes) + +- Remove project serialization in quick actions response. +- Fixed ability to see private groups by users not belonging to given group. + + ## 11.7.5 (2019-02-06) ### Fixed (8 changes) @@ -18,7 +18,7 @@ gem 'gitlab-default_value_for', '~> 3.1.1', require: 'default_value_for' gem 'mysql2', '~> 0.4.10', group: :mysql gem 'pg', '~> 1.1', group: :postgres -gem 'rugged', '~> 0.27' +gem 'rugged', '~> 0.28' gem 'grape-path-helpers', '~> 1.0' gem 'faraday', '~> 0.12' @@ -158,7 +158,7 @@ end gem 'state_machines-activerecord', '~> 0.5.1' # Issue tags -gem 'acts-as-taggable-on', '~> 5.0' +gem 'acts-as-taggable-on', '~> 6.0' # Background jobs gem 'sidekiq', '~> 5.2.1' diff --git a/Gemfile.lock b/Gemfile.lock index ec34d3f9d67..face24489f1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -43,8 +43,8 @@ GEM i18n (>= 0.7, < 2) minitest (~> 5.1) tzinfo (~> 1.1) - acts-as-taggable-on (5.0.0) - activerecord (>= 4.2.8) + acts-as-taggable-on (6.0.0) + activerecord (~> 5.0) adamantium (0.2.0) ice_nine (~> 0.11.0) memoizable (~> 0.4.0) @@ -784,7 +784,7 @@ GEM rubyntlm (0.6.2) rubypants (0.2.0) rubyzip (1.2.2) - rugged (0.27.5) + rugged (0.28.0) safe_yaml (1.0.4) sanitize (4.6.6) crass (~> 1.0.2) @@ -813,7 +813,7 @@ GEM selenium-webdriver (3.12.0) childprocess (~> 0.5) rubyzip (~> 1.2) - sentry-raven (2.7.4) + sentry-raven (2.9.0) faraday (>= 0.7.6, < 1.0) settingslogic (2.0.9) sexp_processor (4.11.0) @@ -948,7 +948,7 @@ DEPENDENCIES RedCloth (~> 4.3.2) ace-rails-ap (~> 4.1.0) activerecord_sane_schema_dumper (= 1.0) - acts-as-taggable-on (~> 5.0) + acts-as-taggable-on (~> 6.0) addressable (~> 2.5.2) akismet (~> 2.0) asana (~> 0.8.1) @@ -1134,7 +1134,7 @@ DEPENDENCIES ruby-progressbar ruby_parser (~> 3.8) rubyzip (~> 1.2.2) - rugged (~> 0.27) + rugged (~> 0.28) sanitize (~> 4.6) sass (~> 3.5) sass-rails (~> 5.0.6) diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue index 1fc2b7fe859..e8f8c09152a 100644 --- a/app/assets/javascripts/diffs/components/app.vue +++ b/app/assets/javascripts/diffs/components/app.vue @@ -19,6 +19,7 @@ import { MIN_TREE_WIDTH, MAX_TREE_WIDTH, TREE_HIDE_STATS_WIDTH, + MR_TREE_SHOW_KEY, } from '../constants'; export default { @@ -162,10 +163,13 @@ export default { 'setHighlightedRow', 'cacheTreeListWidth', 'scrollToFile', + 'toggleShowTreeList', ]), fetchData() { this.fetchDiffFiles() .then(() => { + this.hideTreeListIfJustOneFile(); + requestIdleCallback( () => { this.setDiscussions(); @@ -231,6 +235,13 @@ export default { this.scrollToFile(this.diffFiles[targetIndex].file_path); } }, + hideTreeListIfJustOneFile() { + const storedTreeShow = localStorage.getItem(MR_TREE_SHOW_KEY); + + if ((storedTreeShow === null && this.diffFiles.length <= 1) || storedTreeShow === 'false') { + this.toggleShowTreeList(false); + } + }, }, minTreeWidth: MIN_TREE_WIDTH, maxTreeWidth: MAX_TREE_WIDTH, diff --git a/app/assets/javascripts/diffs/components/tree_list.vue b/app/assets/javascripts/diffs/components/tree_list.vue index 8fc3af15bea..384f33e0983 100644 --- a/app/assets/javascripts/diffs/components/tree_list.vue +++ b/app/assets/javascripts/diffs/components/tree_list.vue @@ -30,8 +30,9 @@ export default { filteredTreeList() { const search = this.search.toLowerCase().trim(); - if (search === '' || this.$options.fuzzyFileFinderEnabled) + if (search === '') { return this.renderTreeList ? this.tree : this.allBlobs; + } return this.allBlobs.reduce((acc, folder) => { const tree = folder.tree.filter(f => f.path.toLowerCase().indexOf(search) >= 0); @@ -51,13 +52,11 @@ export default { }, }, methods: { - ...mapActions('diffs', ['toggleTreeOpen', 'scrollToFile', 'toggleFileFinder']), + ...mapActions('diffs', ['toggleTreeOpen', 'scrollToFile']), clearSearch() { this.search = ''; }, }, - shortcutKeyCharacter: `${/Mac/i.test(navigator.userAgent) ? '⌘' : 'Ctrl'}+P`, - diffTreeFiltering: gon.features && gon.features.diffTreeFiltering, }; </script> @@ -66,36 +65,21 @@ export default { <div class="append-bottom-8 position-relative tree-list-search d-flex"> <div class="flex-fill d-flex"> <icon name="search" class="position-absolute tree-list-icon" /> - <template v-if="$options.diffTreeFiltering"> - <input - v-model="search" - :placeholder="s__('MergeRequest|Filter files')" - type="search" - class="form-control" - /> - <button - v-show="search" - :aria-label="__('Clear search')" - type="button" - class="position-absolute bg-transparent tree-list-icon tree-list-clear-icon border-0 p-0" - @click="clearSearch" - > - <icon name="close" /> - </button> - </template> - <template v-else> - <button - type="button" - class="form-control text-left text-secondary" - @click="toggleFileFinder(true)" - > - {{ s__('MergeRequest|Search files') }} - </button> - <span - class="position-absolute text-secondary diff-tree-search-shortcut" - v-html="$options.shortcutKeyCharacter" - ></span> - </template> + <input + v-model="search" + :placeholder="s__('MergeRequest|Filter files')" + type="search" + class="form-control" + /> + <button + v-show="search" + :aria-label="__('Clear search')" + type="button" + class="position-absolute bg-transparent tree-list-icon tree-list-clear-icon border-0 p-0" + @click="clearSearch" + > + <icon name="close" /> + </button> </div> </div> <div :class="{ 'pt-0 tree-list-blobs': !renderTreeList }" class="tree-list-scroll"> diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js index 4a04216d893..b58ae0d248c 100644 --- a/app/assets/javascripts/diffs/store/actions.js +++ b/app/assets/javascripts/diffs/store/actions.js @@ -266,9 +266,12 @@ export const scrollToFile = ({ state, commit }, path) => { commit(types.UPDATE_CURRENT_DIFF_FILE_ID, fileHash); }; -export const toggleShowTreeList = ({ commit, state }) => { +export const toggleShowTreeList = ({ commit, state }, saving = true) => { commit(types.TOGGLE_SHOW_TREE_LIST); - localStorage.setItem(MR_TREE_SHOW_KEY, state.showTreeList); + + if (saving) { + localStorage.setItem(MR_TREE_SHOW_KEY, state.showTreeList); + } }; export const openDiffFileCommentForm = ({ commit, getters }, formData) => { diff --git a/app/assets/javascripts/diffs/store/modules/diff_state.js b/app/assets/javascripts/diffs/store/modules/diff_state.js index 47f78a5db54..cf4dd93dbfb 100644 --- a/app/assets/javascripts/diffs/store/modules/diff_state.js +++ b/app/assets/javascripts/diffs/store/modules/diff_state.js @@ -1,13 +1,10 @@ import Cookies from 'js-cookie'; import { getParameterValues } from '~/lib/utils/url_utility'; -import bp from '~/breakpoints'; -import { parseBoolean } from '~/lib/utils/common_utils'; -import { INLINE_DIFF_VIEW_TYPE, DIFF_VIEW_COOKIE_NAME, MR_TREE_SHOW_KEY } from '../../constants'; +import { INLINE_DIFF_VIEW_TYPE, DIFF_VIEW_COOKIE_NAME } from '../../constants'; const viewTypeFromQueryString = getParameterValues('view')[0]; const viewTypeFromCookie = Cookies.get(DIFF_VIEW_COOKIE_NAME); const defaultViewType = INLINE_DIFF_VIEW_TYPE; -const storedTreeShow = localStorage.getItem(MR_TREE_SHOW_KEY); export default () => ({ isLoading: true, @@ -23,8 +20,7 @@ export default () => ({ diffViewType: viewTypeFromQueryString || viewTypeFromCookie || defaultViewType, tree: [], treeEntries: {}, - showTreeList: - storedTreeShow === null ? bp.getBreakpointSize() !== 'xs' : parseBoolean(storedTreeShow), + showTreeList: true, currentDiffFileId: '', projectPath: '', commentForms: [], diff --git a/app/assets/javascripts/filtered_search/visual_token_value.js b/app/assets/javascripts/filtered_search/visual_token_value.js index 7f6f41c18f7..24532d88cf3 100644 --- a/app/assets/javascripts/filtered_search/visual_token_value.js +++ b/app/assets/javascripts/filtered_search/visual_token_value.js @@ -13,9 +13,9 @@ export default class VisualTokenValue { } render(tokenValueContainer, tokenValueElement) { - const { tokenType } = this; + const { tokenType, tokenValue } = this; - if (['none', 'any'].includes(tokenType)) { + if (['none', 'any'].includes(tokenValue.toLowerCase())) { return; } diff --git a/app/assets/javascripts/mirrors/ssh_mirror.js b/app/assets/javascripts/mirrors/ssh_mirror.js index 5bdf5d6277a..547c078ec55 100644 --- a/app/assets/javascripts/mirrors/ssh_mirror.js +++ b/app/assets/javascripts/mirrors/ssh_mirror.js @@ -20,6 +20,7 @@ export default class SSHMirror { this.$btnDetectHostKeys = this.$form.find('.js-detect-host-keys'); this.$btnSSHHostsShowAdvanced = this.$form.find('.btn-show-advanced'); this.$dropdownAuthType = this.$form.find('.js-mirror-auth-type'); + this.$hiddenAuthType = this.$form.find('.js-hidden-mirror-auth-type'); this.$wellAuthTypeChanging = this.$form.find('.js-well-changing-auth'); this.$wellPasswordAuth = this.$form.find('.js-well-password-auth'); @@ -167,6 +168,7 @@ export default class SSHMirror { this.$wellPasswordAuth.collapse('hide'); this.$wellSSHAuth.collapse('hide'); + this.updateHiddenAuthType(selectedAuthType); // This request should happen only if selected Auth type was SSH // and SSH Public key was not present on page load @@ -234,6 +236,12 @@ export default class SSHMirror { toggleAuthWell(authType) { this.$wellPasswordAuth.collapse(authType === AUTH_METHOD.PASSWORD ? 'show' : 'hide'); this.$wellSSHAuth.collapse(authType === AUTH_METHOD.SSH ? 'show' : 'hide'); + this.updateHiddenAuthType(authType); + } + + updateHiddenAuthType(authType) { + this.$hiddenAuthType.val(authType); + this.$hiddenAuthType.prop('disabled', authType === AUTH_METHOD.SSH); } /** diff --git a/app/assets/javascripts/notes/components/discussion_filter.vue b/app/assets/javascripts/notes/components/discussion_filter.vue index 31164f74201..47951591e82 100644 --- a/app/assets/javascripts/notes/components/discussion_filter.vue +++ b/app/assets/javascripts/notes/components/discussion_filter.vue @@ -22,7 +22,7 @@ export default { }, selectedValue: { type: Number, - default: null, + default: DISCUSSION_FILTERS_DEFAULT_VALUE, required: false, }, }, diff --git a/app/assets/javascripts/notes/components/note_actions/reply_button.vue b/app/assets/javascripts/notes/components/note_actions/reply_button.vue index f50cab81efe..be8e42af9ea 100644 --- a/app/assets/javascripts/notes/components/note_actions/reply_button.vue +++ b/app/assets/javascripts/notes/components/note_actions/reply_button.vue @@ -18,7 +18,7 @@ export default { <div class="note-actions-item"> <gl-button ref="button" - v-gl-tooltip.bottom + v-gl-tooltip class="note-action-button" variant="transparent" :title="__('Reply to comment')" diff --git a/app/assets/javascripts/notes/discussion_filters.js b/app/assets/javascripts/notes/discussion_filters.js index 5c5f38a3fb0..cdf9a46c5aa 100644 --- a/app/assets/javascripts/notes/discussion_filters.js +++ b/app/assets/javascripts/notes/discussion_filters.js @@ -6,12 +6,16 @@ export default store => { if (discussionFilterEl) { const { defaultFilter, notesFilters } = discussionFilterEl.dataset; - const selectedValue = defaultFilter ? parseInt(defaultFilter, 10) : null; const filterValues = notesFilters ? JSON.parse(notesFilters) : {}; const filters = Object.keys(filterValues).map(entry => ({ title: entry, value: filterValues[entry], })); + const props = { filters }; + + if (defaultFilter) { + props.selectedValue = parseInt(defaultFilter, 10); + } return new Vue({ el: discussionFilterEl, @@ -21,12 +25,7 @@ export default store => { }, store, render(createElement) { - return createElement('discussion-filter', { - props: { - filters, - selectedValue, - }, - }); + return createElement('discussion-filter', { props }); }, }); } diff --git a/app/assets/javascripts/notes/index.js b/app/assets/javascripts/notes/index.js index 4883266dae5..30372103590 100644 --- a/app/assets/javascripts/notes/index.js +++ b/app/assets/javascripts/notes/index.js @@ -6,9 +6,8 @@ import createStore from './stores'; document.addEventListener('DOMContentLoaded', () => { const store = createStore(); - initDiscussionFilters(store); - - return new Vue({ + // eslint-disable-next-line no-new + new Vue({ el: '#js-vue-notes', components: { notesApp, @@ -49,4 +48,6 @@ document.addEventListener('DOMContentLoaded', () => { }); }, }); + + initDiscussionFilters(store); }); diff --git a/app/assets/stylesheets/framework/blank.scss b/app/assets/stylesheets/framework/blank.scss index 91dbb2a6365..cbd390e7145 100644 --- a/app/assets/stylesheets/framework/blank.scss +++ b/app/assets/stylesheets/framework/blank.scss @@ -69,6 +69,7 @@ @include media-breakpoint-up(sm) { display: flex; + height: 100%; align-items: center; padding: 50px 30px; } @@ -99,3 +100,30 @@ } } } + +@include media-breakpoint-up(lg) { + .column-large { + flex: 2; + } + + .column-small { + flex: 1; + margin-bottom: 15px; + + .blank-state { + max-width: 400px; + flex-wrap: wrap; + margin-left: 15px; + } + + .blank-state-icon { + margin-bottom: 30px; + } + } +} + +@include media-breakpoint-down(xs) { + .blank-state-icon svg { + width: 315px; + } +} diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index d47931a49e4..97a9a55c968 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -120,7 +120,7 @@ hr { text-overflow: ellipsis; white-space: nowrap; - > div, + > div:not(.block), .str-truncated { display: inline; } @@ -381,6 +381,7 @@ img.emoji { .prepend-left-5 { margin-left: 5px; } .prepend-left-8 { margin-left: 8px; } .prepend-left-10 { margin-left: 10px; } +.prepend-left-15 { margin-left: 15px; } .prepend-left-default { margin-left: $gl-padding; } .prepend-left-20 { margin-left: 20px; } .prepend-left-32 { margin-left: 32px; } @@ -388,6 +389,7 @@ img.emoji { .append-right-5 { margin-right: 5px; } .append-right-8 { margin-right: 8px; } .append-right-10 { margin-right: 10px; } +.append-right-15 { margin-right: 15px; } .append-right-default { margin-right: $gl-padding; } .append-right-20 { margin-right: 20px; } .prepend-right-32 { margin-right: 32px; } @@ -402,6 +404,8 @@ img.emoji { .prepend-bottom-32 { margin-bottom: 32px; } .inline { display: inline-block; } .center { text-align: center; } +.block { display: block; } +.flex { display: flex; } .vertical-align-middle { vertical-align: middle; } .vertical-align-sub { vertical-align: sub; } .flex-align-self-center { align-self: center; } @@ -411,12 +415,6 @@ img.emoji { .ws-normal { white-space: normal; } .overflow-auto { overflow: auto; } -.d-flex-center { - display: flex; - align-items: center; - justify-content: center; -} - /** COMMON SIZING CLASSES **/ .w-0 { width: 0; } diff --git a/app/assets/stylesheets/framework/system_messages.scss b/app/assets/stylesheets/framework/system_messages.scss index 3d66136938f..e5edddec71e 100644 --- a/app/assets/stylesheets/framework/system_messages.scss +++ b/app/assets/stylesheets/framework/system_messages.scss @@ -12,7 +12,7 @@ p { @include str-truncated(100%); - margin-top: 0; + margin-top: -1px; margin-bottom: 0; } } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 0333b9445c5..08dbe3d5b0f 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -277,7 +277,7 @@ $general-hover-transition-duration: 100ms; $general-hover-transition-curve: linear; $highlight-changes-color: rgb(235, 255, 232); $performance-bar-height: 35px; -$system-header-height: 35px; +$system-header-height: 16px; $system-footer-height: $system-header-height; $flash-height: 52px; $context-header-height: 60px; diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index fa5a182243c..916f6cd3137 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -50,7 +50,6 @@ position: relative; } - .build-trace { @include build-trace(); } @@ -392,3 +391,14 @@ right: 0; margin-top: -17px; } + +@include media-breakpoint-down(sm) { + .top-bar { + .truncated-info { + white-space: nowrap; + overflow: hidden; + max-width: 220px; + text-overflow: ellipsis; + } + } +} diff --git a/app/assets/stylesheets/pages/import.scss b/app/assets/stylesheets/pages/import.scss index 7f800367cad..20240835fda 100644 --- a/app/assets/stylesheets/pages/import.scss +++ b/app/assets/stylesheets/pages/import.scss @@ -49,3 +49,15 @@ .import-projects-loading-icon { margin-top: $gl-padding-32; } + +.btn-import { + .loading-icon { + display: none; + } + + &.is-loading { + .loading-icon { + display: inline-block; + } + } +} diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 44556060c65..df3a4be6559 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -806,7 +806,7 @@ .merge-request-tabs-holder { top: $header-height; - z-index: 300; + z-index: 250; background-color: $white-light; border-bottom: 1px solid $border-color; diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 2b6319ddd4f..ef9c87d0452 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -498,7 +498,8 @@ list-style: none; } - &:last-child { + // when downstream pipelines are present, the last stage isn't the last column + &:last-child:not(.has-downstream) { .build { // Remove right connecting horizontal line from first build in last stage &:first-child::after { @@ -515,7 +516,8 @@ } } - &:first-child { + // when upstream pipelines are present, the first stage isn't the first column + &:first-child:not(.has-upstream) { .build { // Remove left curved connectors from all builds in first stage &:not(:first-child)::before { diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index ab26259c007..8e933b62dd9 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -266,21 +266,6 @@ padding-top: 20px; } - .cover-controls { - position: static; - padding: 0 16px; - margin-bottom: 20px; - display: flex; - - .btn { - flex-grow: 1; - - &:first-child { - margin-left: 0; - } - } - } - .user-profile-nav { a { margin-right: 0; diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb index 550f29a58d2..3fa61c7b117 100644 --- a/app/controllers/admin/projects_controller.rb +++ b/app/controllers/admin/projects_controller.rb @@ -15,7 +15,7 @@ class Admin::ProjectsController < Admin::ApplicationController format.html format.json do render json: { - html: view_to_html_string("admin/projects/_projects", locals: { projects: @projects }) + html: view_to_html_string("admin/projects/_projects", projects: @projects) } end end diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb index b4fee93713b..f96d1821095 100644 --- a/app/controllers/concerns/notes_actions.rb +++ b/app/controllers/concerns/notes_actions.rb @@ -48,7 +48,7 @@ module NotesActions respond_to do |format| format.json do json = { - commands_changes: @note.commands_changes + commands_changes: @note.commands_changes&.slice(:emoji_award, :time_estimate, :spend_time) } if @note.persisted? && return_discussion? diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb index b044affd4e8..0a47736cad8 100644 --- a/app/controllers/dashboard/projects_controller.rb +++ b/app/controllers/dashboard/projects_controller.rb @@ -26,7 +26,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController end format.json do render json: { - html: view_to_html_string("dashboard/projects/_projects", locals: { projects: @projects }) + html: view_to_html_string("dashboard/projects/_projects", projects: @projects) } end end @@ -43,7 +43,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController format.html format.json do render json: { - html: view_to_html_string("dashboard/projects/_projects", locals: { projects: @projects }) + html: view_to_html_string("dashboard/projects/_projects", projects: @projects) } end end diff --git a/app/controllers/explore/projects_controller.rb b/app/controllers/explore/projects_controller.rb index f3d76c5a478..ef86d5f981a 100644 --- a/app/controllers/explore/projects_controller.rb +++ b/app/controllers/explore/projects_controller.rb @@ -15,7 +15,7 @@ class Explore::ProjectsController < Explore::ApplicationController format.html format.json do render json: { - html: view_to_html_string("explore/projects/_projects", locals: { projects: @projects }) + html: view_to_html_string("explore/projects/_projects", projects: @projects) } end end @@ -30,7 +30,7 @@ class Explore::ProjectsController < Explore::ApplicationController format.html format.json do render json: { - html: view_to_html_string("explore/projects/_projects", locals: { projects: @projects }) + html: view_to_html_string("explore/projects/_projects", projects: @projects) } end end @@ -44,7 +44,7 @@ class Explore::ProjectsController < Explore::ApplicationController format.html format.json do render json: { - html: view_to_html_string("explore/projects/_projects", locals: { projects: @projects }) + html: view_to_html_string("explore/projects/_projects", projects: @projects) } end end diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb index 0bc082246a1..f1d6fb00cfc 100644 --- a/app/controllers/groups/group_members_controller.rb +++ b/app/controllers/groups/group_members_controller.rb @@ -12,6 +12,7 @@ class Groups::GroupMembersController < Groups::ApplicationController # Authorize before_action :authorize_admin_group_member!, except: admin_not_required_endpoints + skip_before_action :check_two_factor_requirement, only: :leave skip_cross_project_access_check :index, :create, :update, :destroy, :request_access, :approve_access_request, :leave, :resend_invite, :override diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb index cc2bb99f55b..e90e8278c13 100644 --- a/app/controllers/omniauth_callbacks_controller.rb +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -3,6 +3,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController include AuthenticatesWithTwoFactor include Devise::Controllers::Rememberable + include AuthHelper protect_from_forgery except: [:kerberos, :saml, :cas3, :failure], with: :exception, prepend: true @@ -80,10 +81,11 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController end if current_user + return render_403 unless link_provider_allowed?(oauth['provider']) + log_audit_event(current_user, with: oauth['provider']) identity_linker ||= auth_module::IdentityLinker.new(current_user, oauth) - identity_linker.link if identity_linker.changed? diff --git a/app/controllers/profiles/accounts_controller.rb b/app/controllers/profiles/accounts_controller.rb index b0d65f284af..0d2a6145d0e 100644 --- a/app/controllers/profiles/accounts_controller.rb +++ b/app/controllers/profiles/accounts_controller.rb @@ -14,7 +14,7 @@ class Profiles::AccountsController < Profiles::ApplicationController return render_404 unless identity - if unlink_allowed?(provider) + if unlink_provider_allowed?(provider) identity.destroy else flash[:alert] = "You are not allowed to unlink your primary login account" diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb index ba94196b2f9..83e14275a8b 100644 --- a/app/controllers/profiles/two_factor_auths_controller.rb +++ b/app/controllers/profiles/two_factor_auths_controller.rb @@ -18,21 +18,16 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController two_factor_authentication_reason( global: lambda do flash.now[:alert] = - 'The global settings require you to enable Two-Factor Authentication for your account.' + s_('The global settings require you to enable Two-Factor Authentication for your account.') end, group: lambda do |groups| - group_links = groups.map { |group| view_context.link_to group.full_name, group_path(group) }.to_sentence - - flash.now[:alert] = %{ - The group settings for #{group_links} require you to enable - Two-Factor Authentication for your account. - }.html_safe + flash.now[:alert] = groups_notification(groups) end ) unless two_factor_grace_period_expired? grace_period_deadline = current_user.otp_grace_period_started_at + two_factor_grace_period.hours - flash.now[:alert] = flash.now[:alert] + " You need to do this before #{l(grace_period_deadline)}." + flash.now[:alert] = flash.now[:alert] + s_(" You need to do this before %{grace_period_deadline}.") % { grace_period_deadline: l(grace_period_deadline) } end end @@ -49,7 +44,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController render 'create' else - @error = 'Invalid pin code' + @error = s_('Invalid pin code') @qr_code = build_qr_code setup_u2f_registration render 'show' @@ -63,7 +58,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController if @u2f_registration.persisted? session.delete(:challenges) - redirect_to profile_two_factor_auth_path, notice: "Your U2F device was registered!" + redirect_to profile_two_factor_auth_path, notice: s_("Your U2F device was registered!") else @qr_code = build_qr_code setup_u2f_registration @@ -85,7 +80,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController def skip if two_factor_grace_period_expired? - redirect_to new_profile_two_factor_auth_path, alert: 'Cannot skip two factor authentication setup' + redirect_to new_profile_two_factor_auth_path, alert: s_('Cannot skip two factor authentication setup') else session[:skip_two_factor] = current_user.otp_grace_period_started_at + two_factor_grace_period.hours redirect_to root_path @@ -126,4 +121,12 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController def u2f_registration_params params.require(:u2f_registration).permit(:device_response, :name) end + + def groups_notification(groups) + group_links = groups.map { |group| view_context.link_to group.full_name, group_path(group) }.to_sentence + leave_group_links = groups.map { |group| view_context.link_to (s_("leave %{group_name}") % { group_name: group.full_name }), leave_group_members_path(group), remote: false, method: :delete}.to_sentence + + s_(%{The group settings for %{group_links} require you to enable Two-Factor Authentication for your account. You can %{leave_group_links}.}) + .html_safe % { group_links: group_links.html_safe, leave_group_links: leave_group_links.html_safe } + end end diff --git a/app/controllers/projects/git_http_controller.rb b/app/controllers/projects/git_http_controller.rb index 0c5328fc941..f28af42d1b7 100644 --- a/app/controllers/projects/git_http_controller.rb +++ b/app/controllers/projects/git_http_controller.rb @@ -4,6 +4,7 @@ class Projects::GitHttpController < Projects::GitHttpClientController include WorkhorseRequest before_action :access_check + prepend_before_action :deny_head_requests, only: [:info_refs] rescue_from Gitlab::GitAccess::UnauthorizedError, with: :render_403 rescue_from Gitlab::GitAccess::NotFoundError, with: :render_404 @@ -32,6 +33,10 @@ class Projects::GitHttpController < Projects::GitHttpClientController private + def deny_head_requests + head :forbidden if request.head? + end + def download_request? upload_pack? end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 2903f7d705b..2b78abc66df 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -17,7 +17,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo before_action :check_user_can_push_to_source_branch!, only: [:rebase] before_action only: [:show] do - push_frontend_feature_flag(:diff_tree_filtering, default_enabled: true) push_frontend_feature_flag(:expand_diff_full_file) end diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index 1b22907c10f..90d4bc674d9 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -29,6 +29,7 @@ class SearchController < ApplicationController @search_objects = search_service.search_objects render_commits if @scope == 'commits' + eager_load_user_status if @scope == 'users' check_single_commit_result end @@ -54,6 +55,12 @@ class SearchController < ApplicationController @search_objects = prepare_commits_for_rendering(@search_objects) end + def eager_load_user_status + return if Feature.disabled?(:users_search, default_enabled: true) + + @search_objects = @search_objects.eager_load(:status) # rubocop:disable CodeReuse/ActiveRecord + end + def check_single_commit_result if @search_results.single_commit_result? only_commit = @search_results.objects('commits').first diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 072d07e0ed2..6eab8c5ee51 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -496,7 +496,7 @@ class IssuableFinder upcoming_ids = Milestone.upcoming_ids(projects, related_groups) items = items.left_joins_milestones.where(milestone_id: upcoming_ids) elsif filter_by_started_milestone? - items = items.left_joins_milestones.where('milestones.start_date <= NOW()') + items = items.left_joins_milestones.merge(Milestone.started) else items = items.with_milestone(params[:milestone_title]) end diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb index 2b1d6f49878..b4ee648361c 100644 --- a/app/helpers/auth_helper.rb +++ b/app/helpers/auth_helper.rb @@ -100,8 +100,12 @@ module AuthHelper end # rubocop: enable CodeReuse/ActiveRecord - def unlink_allowed?(provider) - %w(saml cas3).exclude?(provider.to_s) + def unlink_provider_allowed?(provider) + IdentityProviderPolicy.new(current_user, provider).can?(:unlink) + end + + def link_provider_allowed?(provider) + IdentityProviderPolicy.new(current_user, provider).can?(:link) end extend self diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 2ac8ddc5244..f2abb241753 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -169,7 +169,7 @@ module ProjectsHelper translation.html_safe end - def project_list_cache_key(project) + def project_list_cache_key(project, pipeline_status: true) key = [ project.route.cache_key, project.cache_key, @@ -179,10 +179,11 @@ module ProjectsHelper Gitlab::CurrentSettings.cache_key, "cross-project:#{can?(current_user, :read_cross_project)}", max_project_member_access_cache_key(project), + pipeline_status, 'v2.6' ] - key << pipeline_status_cache_key(project.pipeline_status) if project.pipeline_status.has_status? + key << pipeline_status_cache_key(project.pipeline_status) if pipeline_status && project.pipeline_status.has_status? key end @@ -364,7 +365,8 @@ module ProjectsHelper blobs: :download_code, commits: :download_code, merge_requests: :read_merge_request, - notes: [:read_merge_request, :download_code, :read_issue, :read_project_snippet] + notes: [:read_merge_request, :download_code, :read_issue, :read_project_snippet], + members: :read_project_member ) end diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index 0ee76a51f7d..8110377850b 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -201,4 +201,14 @@ module SearchHelper def limited_count(count, limit = 1000) count > limit ? "#{limit}+" : count end + + def search_tabs?(tab) + return false if Feature.disabled?(:users_search, default_enabled: true) + + if @project + project_search_tabs?(:members) + else + can?(current_user, :read_users_list) + end + end end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index cd36c963ee5..9cc7c0a1b97 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -7,20 +7,14 @@ class ApplicationSetting < ActiveRecord::Base include IgnorableColumn include ChronicDurationAttribute - add_authentication_token_field :runners_registration_token, encrypted: -> { Feature.enabled?(:application_settings_tokens_optional_encryption) ? :optional : :required } + add_authentication_token_field :runners_registration_token, encrypted: -> { Feature.enabled?(:application_settings_tokens_optional_encryption, default_enabled: true) ? :optional : :required } add_authentication_token_field :health_check_access_token - DOMAIN_LIST_SEPARATOR = %r{\s*[,;]\s* # comma or semicolon, optionally surrounded by whitespace - | # or - \s # any whitespace character - | # or - [\r\n] # any number of newline characters - }x - - # Setting a key restriction to `-1` means that all keys of this type are - # forbidden. - FORBIDDEN_KEY_VALUE = KeyRestrictionValidator::FORBIDDEN - SUPPORTED_KEY_TYPES = %i[rsa dsa ecdsa ed25519].freeze + # Include here so it can override methods from + # `add_authentication_token_field` + # We don't prepend for now because otherwise we'll need to + # fix a lot of tests using allow_any_instance_of + include ApplicationSettingImplementation serialize :restricted_visibility_levels # rubocop:disable Cop/ActiveRecordSerialize serialize :import_sources # rubocop:disable Cop/ActiveRecordSerialize @@ -42,8 +36,6 @@ class ApplicationSetting < ActiveRecord::Base cache_markdown_field :shared_runners_text, pipeline: :plain_markdown cache_markdown_field :after_sign_up_text - attr_accessor :domain_whitelist_raw, :domain_blacklist_raw - default_value_for :id, 1 chronic_duration_attr_writer :archive_builds_in_human_readable, :archive_builds_in_seconds @@ -231,266 +223,4 @@ class ApplicationSetting < ActiveRecord::Base reset_memoized_terms end after_commit :expire_performance_bar_allowed_user_ids_cache, if: -> { previous_changes.key?('performance_bar_allowed_group_id') } - - def self.defaults - { - after_sign_up_text: nil, - akismet_enabled: false, - allow_local_requests_from_hooks_and_services: false, - authorized_keys_enabled: true, # TODO default to false if the instance is configured to use AuthorizedKeysCommand - container_registry_token_expire_delay: 5, - default_artifacts_expire_in: '30 days', - default_branch_protection: Settings.gitlab['default_branch_protection'], - default_group_visibility: Settings.gitlab.default_projects_features['visibility_level'], - default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'], - default_projects_limit: Settings.gitlab['default_projects_limit'], - default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'], - disabled_oauth_sign_in_sources: [], - domain_whitelist: Settings.gitlab['domain_whitelist'], - dsa_key_restriction: 0, - ecdsa_key_restriction: 0, - ed25519_key_restriction: 0, - first_day_of_week: 0, - gitaly_timeout_default: 55, - gitaly_timeout_fast: 10, - gitaly_timeout_medium: 30, - gravatar_enabled: Settings.gravatar['enabled'], - help_page_hide_commercial_content: false, - help_page_text: nil, - hide_third_party_offers: false, - housekeeping_bitmaps_enabled: true, - housekeeping_enabled: true, - housekeeping_full_repack_period: 50, - housekeeping_gc_period: 200, - housekeeping_incremental_repack_period: 10, - import_sources: Settings.gitlab['import_sources'], - max_artifacts_size: Settings.artifacts['max_size'], - max_attachment_size: Settings.gitlab['max_attachment_size'], - mirror_available: true, - password_authentication_enabled_for_git: true, - password_authentication_enabled_for_web: Settings.gitlab['signin_enabled'], - performance_bar_allowed_group_id: nil, - rsa_key_restriction: 0, - plantuml_enabled: false, - plantuml_url: nil, - polling_interval_multiplier: 1, - project_export_enabled: true, - recaptcha_enabled: false, - repository_checks_enabled: true, - repository_storages: ['default'], - require_two_factor_authentication: false, - restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'], - session_expire_delay: Settings.gitlab['session_expire_delay'], - send_user_confirmation_email: false, - shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'], - shared_runners_text: nil, - sign_in_text: nil, - signup_enabled: Settings.gitlab['signup_enabled'], - terminal_max_session_time: 0, - throttle_authenticated_api_enabled: false, - throttle_authenticated_api_period_in_seconds: 3600, - throttle_authenticated_api_requests_per_period: 7200, - throttle_authenticated_web_enabled: false, - throttle_authenticated_web_period_in_seconds: 3600, - throttle_authenticated_web_requests_per_period: 7200, - throttle_unauthenticated_enabled: false, - throttle_unauthenticated_period_in_seconds: 3600, - throttle_unauthenticated_requests_per_period: 3600, - two_factor_grace_period: 48, - unique_ips_limit_enabled: false, - unique_ips_limit_per_user: 10, - unique_ips_limit_time_window: 3600, - usage_ping_enabled: Settings.gitlab['usage_ping_enabled'], - instance_statistics_visibility_private: false, - user_default_external: false, - user_default_internal_regex: nil, - user_show_add_ssh_key_message: true, - usage_stats_set_by_user_id: nil, - diff_max_patch_bytes: Gitlab::Git::Diff::DEFAULT_MAX_PATCH_BYTES, - commit_email_hostname: default_commit_email_hostname, - protected_ci_variables: false, - local_markdown_version: 0 - } - end - - def self.default_commit_email_hostname - "users.noreply.#{Gitlab.config.gitlab.host}" - end - - def self.create_from_defaults - build_from_defaults.tap(&:save) - end - - def self.human_attribute_name(attr, _options = {}) - if attr == :default_artifacts_expire_in - 'Default artifacts expiration' - else - super - end - end - - def home_page_url_column_exists? - ::Gitlab::Database.cached_column_exists?(:application_settings, :home_page_url) - end - - def help_page_support_url_column_exists? - ::Gitlab::Database.cached_column_exists?(:application_settings, :help_page_support_url) - end - - def disabled_oauth_sign_in_sources=(sources) - sources = (sources || []).map(&:to_s) & Devise.omniauth_providers.map(&:to_s) - super(sources) - end - - def domain_whitelist_raw - self.domain_whitelist&.join("\n") - end - - def domain_blacklist_raw - self.domain_blacklist&.join("\n") - end - - def domain_whitelist_raw=(values) - self.domain_whitelist = [] - self.domain_whitelist = values.split(DOMAIN_LIST_SEPARATOR) - self.domain_whitelist.reject! { |d| d.empty? } - self.domain_whitelist - end - - def domain_blacklist_raw=(values) - self.domain_blacklist = [] - self.domain_blacklist = values.split(DOMAIN_LIST_SEPARATOR) - self.domain_blacklist.reject! { |d| d.empty? } - self.domain_blacklist - end - - def domain_blacklist_file=(file) - self.domain_blacklist_raw = file.read - end - - def repository_storages - Array(read_attribute(:repository_storages)) - end - - def commit_email_hostname - super.presence || self.class.default_commit_email_hostname - end - - def default_project_visibility=(level) - super(Gitlab::VisibilityLevel.level_value(level)) - end - - def default_snippet_visibility=(level) - super(Gitlab::VisibilityLevel.level_value(level)) - end - - def default_group_visibility=(level) - super(Gitlab::VisibilityLevel.level_value(level)) - end - - def restricted_visibility_levels=(levels) - super(levels&.map { |level| Gitlab::VisibilityLevel.level_value(level) }) - end - - def strip_sentry_values - sentry_dsn.strip! if sentry_dsn.present? - clientside_sentry_dsn.strip! if clientside_sentry_dsn.present? - end - - def performance_bar_allowed_group - Group.find_by_id(performance_bar_allowed_group_id) - end - - # Return true if the Performance Bar is enabled for a given group - def performance_bar_enabled - performance_bar_allowed_group_id.present? - end - - # Choose one of the available repository storage options. Currently all have - # equal weighting. - def pick_repository_storage - repository_storages.sample - end - - def runners_registration_token - ensure_runners_registration_token! - end - - def health_check_access_token - ensure_health_check_access_token! - end - - def usage_ping_can_be_configured? - Settings.gitlab.usage_ping_enabled - end - - def usage_ping_enabled - usage_ping_can_be_configured? && super - end - - def allowed_key_types - SUPPORTED_KEY_TYPES.select do |type| - key_restriction_for(type) != FORBIDDEN_KEY_VALUE - end - end - - def key_restriction_for(type) - attr_name = "#{type}_key_restriction" - - has_attribute?(attr_name) ? public_send(attr_name) : FORBIDDEN_KEY_VALUE # rubocop:disable GitlabSecurity/PublicSend - end - - def allow_signup? - signup_enabled? && password_authentication_enabled_for_web? - end - - def password_authentication_enabled? - password_authentication_enabled_for_web? || password_authentication_enabled_for_git? - end - - def user_default_internal_regex_enabled? - user_default_external? && user_default_internal_regex.present? - end - - def user_default_internal_regex_instance - Regexp.new(user_default_internal_regex, Regexp::IGNORECASE) - end - - delegate :terms, to: :latest_terms, allow_nil: true - def latest_terms - @latest_terms ||= Term.latest - end - - def reset_memoized_terms - @latest_terms = nil - latest_terms - end - - def archive_builds_older_than - archive_builds_in_seconds.seconds.ago if archive_builds_in_seconds - end - - private - - def ensure_uuid! - return if uuid? - - self.uuid = SecureRandom.uuid - end - - def check_repository_storages - invalid = repository_storages - Gitlab.config.repositories.storages.keys - errors.add(:repository_storages, "can't include: #{invalid.join(", ")}") unless - invalid.empty? - end - - def terms_exist - return unless enforce_terms? - - errors.add(:terms, "You need to set terms to be enforced") unless terms.present? - end - - def expire_performance_bar_allowed_user_ids_cache - Gitlab::PerformanceBar.expire_allowed_user_ids_cache - end end diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb new file mode 100644 index 00000000000..265aa1d4965 --- /dev/null +++ b/app/models/application_setting_implementation.rb @@ -0,0 +1,281 @@ +# frozen_string_literal: true + +module ApplicationSettingImplementation + extend ActiveSupport::Concern + + DOMAIN_LIST_SEPARATOR = %r{\s*[,;]\s* # comma or semicolon, optionally surrounded by whitespace + | # or + \s # any whitespace character + | # or + [\r\n] # any number of newline characters + }x + + # Setting a key restriction to `-1` means that all keys of this type are + # forbidden. + FORBIDDEN_KEY_VALUE = KeyRestrictionValidator::FORBIDDEN + SUPPORTED_KEY_TYPES = %i[rsa dsa ecdsa ed25519].freeze + + class_methods do + def defaults + { + after_sign_up_text: nil, + akismet_enabled: false, + allow_local_requests_from_hooks_and_services: false, + authorized_keys_enabled: true, # TODO default to false if the instance is configured to use AuthorizedKeysCommand + container_registry_token_expire_delay: 5, + default_artifacts_expire_in: '30 days', + default_branch_protection: Settings.gitlab['default_branch_protection'], + default_group_visibility: Settings.gitlab.default_projects_features['visibility_level'], + default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'], + default_projects_limit: Settings.gitlab['default_projects_limit'], + default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'], + disabled_oauth_sign_in_sources: [], + domain_whitelist: Settings.gitlab['domain_whitelist'], + dsa_key_restriction: 0, + ecdsa_key_restriction: 0, + ed25519_key_restriction: 0, + first_day_of_week: 0, + gitaly_timeout_default: 55, + gitaly_timeout_fast: 10, + gitaly_timeout_medium: 30, + gravatar_enabled: Settings.gravatar['enabled'], + help_page_hide_commercial_content: false, + help_page_text: nil, + hide_third_party_offers: false, + housekeeping_bitmaps_enabled: true, + housekeeping_enabled: true, + housekeeping_full_repack_period: 50, + housekeeping_gc_period: 200, + housekeeping_incremental_repack_period: 10, + import_sources: Settings.gitlab['import_sources'], + max_artifacts_size: Settings.artifacts['max_size'], + max_attachment_size: Settings.gitlab['max_attachment_size'], + mirror_available: true, + password_authentication_enabled_for_git: true, + password_authentication_enabled_for_web: Settings.gitlab['signin_enabled'], + performance_bar_allowed_group_id: nil, + rsa_key_restriction: 0, + plantuml_enabled: false, + plantuml_url: nil, + polling_interval_multiplier: 1, + project_export_enabled: true, + recaptcha_enabled: false, + repository_checks_enabled: true, + repository_storages: ['default'], + require_two_factor_authentication: false, + restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'], + session_expire_delay: Settings.gitlab['session_expire_delay'], + send_user_confirmation_email: false, + shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'], + shared_runners_text: nil, + sign_in_text: nil, + signup_enabled: Settings.gitlab['signup_enabled'], + terminal_max_session_time: 0, + throttle_authenticated_api_enabled: false, + throttle_authenticated_api_period_in_seconds: 3600, + throttle_authenticated_api_requests_per_period: 7200, + throttle_authenticated_web_enabled: false, + throttle_authenticated_web_period_in_seconds: 3600, + throttle_authenticated_web_requests_per_period: 7200, + throttle_unauthenticated_enabled: false, + throttle_unauthenticated_period_in_seconds: 3600, + throttle_unauthenticated_requests_per_period: 3600, + two_factor_grace_period: 48, + unique_ips_limit_enabled: false, + unique_ips_limit_per_user: 10, + unique_ips_limit_time_window: 3600, + usage_ping_enabled: Settings.gitlab['usage_ping_enabled'], + instance_statistics_visibility_private: false, + user_default_external: false, + user_default_internal_regex: nil, + user_show_add_ssh_key_message: true, + usage_stats_set_by_user_id: nil, + diff_max_patch_bytes: Gitlab::Git::Diff::DEFAULT_MAX_PATCH_BYTES, + commit_email_hostname: default_commit_email_hostname, + protected_ci_variables: false, + local_markdown_version: 0 + } + end + + def default_commit_email_hostname + "users.noreply.#{Gitlab.config.gitlab.host}" + end + + def create_from_defaults + build_from_defaults.tap(&:save) + end + + def human_attribute_name(attr, _options = {}) + if attr == :default_artifacts_expire_in + 'Default artifacts expiration' + else + super + end + end + end + + def home_page_url_column_exists? + ::Gitlab::Database.cached_column_exists?(:application_settings, :home_page_url) + end + + def help_page_support_url_column_exists? + ::Gitlab::Database.cached_column_exists?(:application_settings, :help_page_support_url) + end + + def disabled_oauth_sign_in_sources=(sources) + sources = (sources || []).map(&:to_s) & Devise.omniauth_providers.map(&:to_s) + super(sources) + end + + def domain_whitelist_raw + self.domain_whitelist&.join("\n") + end + + def domain_blacklist_raw + self.domain_blacklist&.join("\n") + end + + def domain_whitelist_raw=(values) + self.domain_whitelist = [] + self.domain_whitelist = values.split(DOMAIN_LIST_SEPARATOR) + self.domain_whitelist.reject! { |d| d.empty? } + self.domain_whitelist + end + + def domain_blacklist_raw=(values) + self.domain_blacklist = [] + self.domain_blacklist = values.split(DOMAIN_LIST_SEPARATOR) + self.domain_blacklist.reject! { |d| d.empty? } + self.domain_blacklist + end + + def domain_blacklist_file=(file) + self.domain_blacklist_raw = file.read + end + + def repository_storages + Array(read_attribute(:repository_storages)) + end + + def commit_email_hostname + super.presence || self.class.default_commit_email_hostname + end + + def default_project_visibility=(level) + super(Gitlab::VisibilityLevel.level_value(level)) + end + + def default_snippet_visibility=(level) + super(Gitlab::VisibilityLevel.level_value(level)) + end + + def default_group_visibility=(level) + super(Gitlab::VisibilityLevel.level_value(level)) + end + + def restricted_visibility_levels=(levels) + super(levels&.map { |level| Gitlab::VisibilityLevel.level_value(level) }) + end + + def strip_sentry_values + sentry_dsn.strip! if sentry_dsn.present? + clientside_sentry_dsn.strip! if clientside_sentry_dsn.present? + end + + def performance_bar_allowed_group + Group.find_by_id(performance_bar_allowed_group_id) + end + + # Return true if the Performance Bar is enabled for a given group + def performance_bar_enabled + performance_bar_allowed_group_id.present? + end + + # Choose one of the available repository storage options. Currently all have + # equal weighting. + def pick_repository_storage + repository_storages.sample + end + + def runners_registration_token + ensure_runners_registration_token! + end + + def health_check_access_token + ensure_health_check_access_token! + end + + def usage_ping_can_be_configured? + Settings.gitlab.usage_ping_enabled + end + + def usage_ping_enabled + usage_ping_can_be_configured? && super + end + + def allowed_key_types + SUPPORTED_KEY_TYPES.select do |type| + key_restriction_for(type) != FORBIDDEN_KEY_VALUE + end + end + + def key_restriction_for(type) + attr_name = "#{type}_key_restriction" + + has_attribute?(attr_name) ? public_send(attr_name) : FORBIDDEN_KEY_VALUE # rubocop:disable GitlabSecurity/PublicSend + end + + def allow_signup? + signup_enabled? && password_authentication_enabled_for_web? + end + + def password_authentication_enabled? + password_authentication_enabled_for_web? || password_authentication_enabled_for_git? + end + + def user_default_internal_regex_enabled? + user_default_external? && user_default_internal_regex.present? + end + + def user_default_internal_regex_instance + Regexp.new(user_default_internal_regex, Regexp::IGNORECASE) + end + + delegate :terms, to: :latest_terms, allow_nil: true + def latest_terms + @latest_terms ||= ApplicationSetting::Term.latest + end + + def reset_memoized_terms + @latest_terms = nil # rubocop:disable Gitlab/ModuleWithInstanceVariables + latest_terms + end + + def archive_builds_older_than + archive_builds_in_seconds.seconds.ago if archive_builds_in_seconds + end + + private + + def ensure_uuid! + return if uuid? + + self.uuid = SecureRandom.uuid + end + + def check_repository_storages + invalid = repository_storages - Gitlab.config.repositories.storages.keys + errors.add(:repository_storages, "can't include: #{invalid.join(", ")}") unless + invalid.empty? + end + + def terms_exist + return unless enforce_terms? + + errors.add(:terms, "You need to set terms to be enforced") unless terms.present? + end + + def expire_performance_bar_allowed_user_ids_cache + Gitlab::PerformanceBar.expire_allowed_user_ids_cache + end +end diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index ce26ee168ef..43f040a91ae 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -10,7 +10,7 @@ module Ci include FromUnion include TokenAuthenticatable - add_authentication_token_field :token, encrypted: -> { Feature.enabled?(:ci_runners_tokens_optional_encryption) ? :optional : :required } + add_authentication_token_field :token, encrypted: -> { Feature.enabled?(:ci_runners_tokens_optional_encryption, default_enabled: true) ? :optional : :required } enum access_level: { not_protected: 0, diff --git a/app/models/concerns/token_authenticatable.rb b/app/models/concerns/token_authenticatable.rb index f5bb559ceda..8c769be0489 100644 --- a/app/models/concerns/token_authenticatable.rb +++ b/app/models/concerns/token_authenticatable.rb @@ -26,34 +26,41 @@ module TokenAuthenticatable end end - define_method(token_field) do + mod = token_authenticatable_module + + mod.define_method(token_field) do strategy.get_token(self) end - define_method("set_#{token_field}") do |token| + mod.define_method("set_#{token_field}") do |token| strategy.set_token(self, token) end - define_method("ensure_#{token_field}") do + mod.define_method("ensure_#{token_field}") do strategy.ensure_token(self) end # Returns a token, but only saves when the database is in read & write mode - define_method("ensure_#{token_field}!") do + mod.define_method("ensure_#{token_field}!") do strategy.ensure_token!(self) end # Resets the token, but only saves when the database is in read & write mode - define_method("reset_#{token_field}!") do + mod.define_method("reset_#{token_field}!") do strategy.reset_token!(self) end - define_method("#{token_field}_matches?") do |other_token| + mod.define_method("#{token_field}_matches?") do |other_token| token = read_attribute(token_field) token.present? && ActiveSupport::SecurityUtils.variable_size_secure_compare(other_token, token) end end + def token_authenticatable_module + @token_authenticatable_module ||= + const_set(:TokenAuthenticatable, Module.new).tap(&method(:include)) + end + def token_authenticatable_fields @token_authenticatable_fields ||= [] end diff --git a/app/models/group.rb b/app/models/group.rb index 495bfe04499..c77586c4cdc 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -56,7 +56,7 @@ class Group < Namespace validates :two_factor_grace_period, presence: true, numericality: { greater_than_or_equal_to: 0 } - add_authentication_token_field :runners_token, encrypted: -> { Feature.enabled?(:groups_tokens_optional_encryption) ? :optional : :required } + add_authentication_token_field :runners_token, encrypted: -> { Feature.enabled?(:groups_tokens_optional_encryption, default_enabled: true) ? :optional : :required } after_create :post_create_hook after_destroy :post_destroy_hook diff --git a/app/models/milestone.rb b/app/models/milestone.rb index d6f94cad1fb..a3831ae3fa8 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -37,6 +37,7 @@ class Milestone < ActiveRecord::Base scope :active, -> { with_state(:active) } scope :closed, -> { with_state(:closed) } scope :for_projects, -> { where(group: nil).includes(:project) } + scope :started, -> { active.where('milestones.start_date <= CURRENT_DATE') } scope :for_projects_and_groups, -> (projects, groups) do projects = projects.compact if projects.is_a? Array diff --git a/app/models/project.rb b/app/models/project.rb index a3d55d390f4..14fc158ede1 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -84,7 +84,7 @@ class Project < ActiveRecord::Base default_value_for :snippets_enabled, gitlab_config_features.snippets default_value_for :only_allow_merge_if_all_discussions_are_resolved, false - add_authentication_token_field :runners_token, encrypted: -> { Feature.enabled?(:projects_tokens_optional_encryption) ? :optional : :required } + add_authentication_token_field :runners_token, encrypted: -> { Feature.enabled?(:projects_tokens_optional_encryption, default_enabled: true) ? :optional : :required } before_validation :mark_remote_mirrors_for_removal, if: -> { RemoteMirror.table_exists? } diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb index c43bd45a62f..6ea0716c192 100644 --- a/app/models/project_wiki.rb +++ b/app/models/project_wiki.rb @@ -183,7 +183,7 @@ class ProjectWiki end def commit_details(action, message = nil, title = nil) - commit_message = message || default_message(action, title) + commit_message = message.presence || default_message(action, title) git_user = Gitlab::Git::User.from_gitlab(@user) Gitlab::Git::Wiki::CommitDetails.new(@user.id, diff --git a/app/policies/identity_provider_policy.rb b/app/policies/identity_provider_policy.rb new file mode 100644 index 00000000000..d34cdd5bdd4 --- /dev/null +++ b/app/policies/identity_provider_policy.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class IdentityProviderPolicy < BasePolicy + desc "Provider is SAML or CAS3" + condition(:protected_provider, scope: :subject, score: 0) { %w(saml cas3).include?(@subject.to_s) } + + rule { anonymous }.prevent_all + + rule { default }.policy do + enable :unlink + enable :link + end + + rule { protected_provider }.prevent(:unlink) +end diff --git a/app/presenters/ci/pipeline_presenter.rb b/app/presenters/ci/pipeline_presenter.rb index 57daf04efc6..1c1347c5a57 100644 --- a/app/presenters/ci/pipeline_presenter.rb +++ b/app/presenters/ci/pipeline_presenter.rb @@ -3,6 +3,7 @@ module Ci class PipelinePresenter < Gitlab::View::Presenter::Delegated include Gitlab::Utils::StrongMemoize + include ActionView::Helpers::UrlHelper # We use a class method here instead of a constant, allowing EE to redefine # the returned `Hash` more easily. @@ -32,5 +33,57 @@ module Ci "Pipeline is redundant and is auto-canceled by Pipeline ##{auto_canceled_by_id}" end end + + def ref_text + if pipeline.detached_merge_request_pipeline? + _("for %{link_to_merge_request} with %{link_to_merge_request_source_branch}").html_safe % { link_to_merge_request: link_to_merge_request, link_to_merge_request_source_branch: link_to_merge_request_source_branch } + elsif pipeline.merge_request_pipeline? + _("for %{link_to_merge_request} with %{link_to_merge_request_source_branch} into %{link_to_merge_request_target_branch}").html_safe % { link_to_merge_request: link_to_merge_request, link_to_merge_request_source_branch: link_to_merge_request_source_branch, link_to_merge_request_target_branch: link_to_merge_request_target_branch } + elsif pipeline.ref + if pipeline.ref_exists? + _("for %{link_to_pipeline_ref}").html_safe % { link_to_pipeline_ref: link_to_pipeline_ref } + else + _("for %{ref}") % { ref: content_tag(:span, pipeline.ref, class: 'ref-name') } + end + end + end + + def link_to_pipeline_ref + link_to(pipeline.ref, + project_commits_path(pipeline.project, pipeline.ref), + class: "ref-name") + end + + def link_to_merge_request + return unless merge_request_presenter + + link_to(merge_request_presenter.to_reference, + project_merge_request_path(merge_request_presenter.project, merge_request_presenter), + class: 'mr-iid') + end + + def link_to_merge_request_source_branch + return unless merge_request_presenter + + link_to(merge_request_presenter.source_branch, + merge_request_presenter.source_branch_commits_path, + class: 'ref-name') + end + + def link_to_merge_request_target_branch + return unless merge_request_presenter + + link_to(merge_request_presenter.target_branch, + merge_request_presenter.target_branch_commits_path, + class: 'ref-name') + end + + private + + def merge_request_presenter + return unless pipeline.triggered_by_merge_request? + + @merge_request_presenter ||= pipeline.merge_request.present(current_user: current_user) + end end end diff --git a/app/presenters/merge_request_presenter.rb b/app/presenters/merge_request_presenter.rb index af164858408..284b1ad9b55 100644 --- a/app/presenters/merge_request_presenter.rb +++ b/app/presenters/merge_request_presenter.rb @@ -104,6 +104,12 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated end end + def source_branch_commits_path + if source_branch_exists? + project_commits_path(source_project, source_branch) + end + end + def source_branch_path if source_branch_exists? project_branch_path(source_project, source_branch) diff --git a/app/serializers/merge_request_for_pipeline_entity.rb b/app/serializers/merge_request_for_pipeline_entity.rb index 7779ddfd65a..17a5c4ebbf9 100644 --- a/app/serializers/merge_request_for_pipeline_entity.rb +++ b/app/serializers/merge_request_for_pipeline_entity.rb @@ -11,7 +11,7 @@ class MergeRequestForPipelineEntity < Grape::Entity expose :title expose :source_branch - expose :source_branch_path + expose :source_branch_commits_path, as: :source_branch_path expose :target_branch - expose :target_branch_path + expose :target_branch_commits_path, as: :target_branch_path end diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb index e95ba09c006..707caee482c 100644 --- a/app/services/auth/container_registry_authentication_service.rb +++ b/app/services/auth/container_registry_authentication_service.rb @@ -116,7 +116,7 @@ module Auth build_can_pull?(requested_project) || user_can_pull?(requested_project) || deploy_token_can_pull?(requested_project) when 'push' build_can_push?(requested_project) || user_can_push?(requested_project) - when '*' + when '*', 'delete' user_can_admin?(requested_project) else false diff --git a/app/services/search/global_service.rb b/app/services/search/global_service.rb index d6af26d949d..f711839e389 100644 --- a/app/services/search/global_service.rb +++ b/app/services/search/global_service.rb @@ -23,7 +23,8 @@ module Search def allowed_scopes strong_memoize(:allowed_scopes) do - %w[issues merge_requests milestones] + allowed_scopes = %w[issues merge_requests milestones] + allowed_scopes << 'users' if Feature.enabled?(:users_search, default_enabled: true) end end diff --git a/app/services/search/group_service.rb b/app/services/search/group_service.rb index 34803d005e3..6f3b5f00b86 100644 --- a/app/services/search/group_service.rb +++ b/app/services/search/group_service.rb @@ -11,6 +11,12 @@ module Search @group = group end + def execute + Gitlab::GroupSearchResults.new( + current_user, projects, group, params[:search], default_project_filter: default_project_filter + ) + end + def projects return Project.none unless group return @projects if defined? @projects diff --git a/app/services/search/project_service.rb b/app/services/search/project_service.rb index f223c8be103..32d5cd7ddb2 100644 --- a/app/services/search/project_service.rb +++ b/app/services/search/project_service.rb @@ -16,7 +16,12 @@ module Search end def scope - @scope ||= %w[notes issues merge_requests milestones wiki_blobs commits].delete(params[:scope]) { 'blobs' } + @scope ||= begin + allowed_scopes = %w[notes issues merge_requests milestones wiki_blobs commits] + allowed_scopes << 'users' if Feature.enabled?(:users_search, default_enabled: true) + + allowed_scopes.delete(params[:scope]) { 'blobs' } + end end end end diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml index 6756299cf43..2a1d2c2aeab 100644 --- a/app/views/admin/dashboard/index.html.haml +++ b/app/views/admin/dashboard/index.html.haml @@ -22,9 +22,10 @@ %h3.text-center Users: = approximate_count_with_delimiters(@counts, User) - = render_if_exists 'admin/dashboard/users_statistics' %hr - = link_to 'New user', new_admin_user_path, class: "btn btn-success" + .btn-group.d-flex{ role: 'group' } + = link_to 'New user', new_admin_user_path, class: "btn btn-success" + = render_if_exists 'admin/dashboard/users_statistics' .col-sm-4 .info-well.dark-well .well-segment.well-centered diff --git a/app/views/clusters/clusters/_sidebar.html.haml b/app/views/clusters/clusters/_sidebar.html.haml index 6e4415c21a9..60ccad5b943 100644 --- a/app/views/clusters/clusters/_sidebar.html.haml +++ b/app/views/clusters/clusters/_sidebar.html.haml @@ -4,3 +4,5 @@ = clusterable.sidebar_text %p = clusterable.learn_more_link + += render_if_exists 'clusters/multiple_clusters_message' diff --git a/app/views/clusters/clusters/show.html.haml b/app/views/clusters/clusters/show.html.haml index 884fa323093..68d9510e1bf 100644 --- a/app/views/clusters/clusters/show.html.haml +++ b/app/views/clusters/clusters/show.html.haml @@ -34,6 +34,8 @@ = render 'banner' = render 'form' + = render_if_exists 'health' + .cluster-applications-table#js-cluster-applications %section.settings#js-cluster-details{ class: ('expanded' if expanded) } diff --git a/app/views/doorkeeper/applications/index.html.haml b/app/views/doorkeeper/applications/index.html.haml index 1f5c70a6c6e..5d85d9e431f 100644 --- a/app/views/doorkeeper/applications/index.html.haml +++ b/app/views/doorkeeper/applications/index.html.haml @@ -52,7 +52,7 @@ .oauth-authorized-applications.prepend-top-20.append-bottom-default - if user_oauth_applications? %h5 - = _("Authorized applications (%{size})") % { size: @authorized_tokens.size } + = _("Authorized applications (%{size})") % { size: @authorized_apps.size + @authorized_anonymous_tokens.size } - if @authorized_tokens.any? .table-responsive diff --git a/app/views/profiles/_email_settings.html.haml b/app/views/profiles/_email_settings.html.haml new file mode 100644 index 00000000000..fb4da08e129 --- /dev/null +++ b/app/views/profiles/_email_settings.html.haml @@ -0,0 +1,16 @@ +- form = local_assigns.fetch(:form) +- readonly = @user.read_only_attribute?(:email) +- email_change_disabled = local_assigns.fetch(:email_change_disabled, nil) +- read_only_help_text = readonly ? s_("Profiles|Your email address was automatically set based on your %{provider_label} account") % { provider_label: attribute_provider_label(:email) } : user_email_help_text(@user) +- help_text = email_change_disabled ? s_("Your account uses dedicated credentials for the \"%{group_name}\" group and can only be updated through SSO.") % { group_name: @user.managing_group.name } : read_only_help_text + += form.text_field :email, required: true, class: 'input-lg', value: (@user.email unless @user.temp_oauth_email?), help: help_text.html_safe, readonly: readonly || email_change_disabled += form.select :public_email, options_for_select(@user.all_emails, selected: @user.public_email), + { help: s_("Profiles|This email will be displayed on your public profile"), include_blank: s_("Profiles|Do not show on profile") }, + control_class: 'select2 input-lg', disabled: email_change_disabled +- commit_email_link_url = help_page_path('user/profile/index', anchor: 'commit-email', target: '_blank') +- commit_email_link_start = '<a href="%{url}">'.html_safe % { url: commit_email_link_url } +- commit_email_docs_link = s_('Profiles|This email will be used for web based operations, such as edits and merges. %{commit_email_link_start}Learn more%{commit_email_link_end}').html_safe % { commit_email_link_start: commit_email_link_start, commit_email_link_end: '</a>'.html_safe } += form.select :commit_email, options_for_select(commit_email_select_options(@user), selected: selected_commit_email(@user)), + { help: commit_email_docs_link }, + control_class: 'select2 input-lg', disabled: email_change_disabled diff --git a/app/views/profiles/accounts/_providers.html.haml b/app/views/profiles/accounts/_providers.html.haml new file mode 100644 index 00000000000..068f9cc70f7 --- /dev/null +++ b/app/views/profiles/accounts/_providers.html.haml @@ -0,0 +1,21 @@ +%label.label-bold + = s_('Profiles|Connected Accounts') + %p= s_('Profiles|Click on icon to activate signin with one of the following services') + - providers.each do |provider| + - unlink_allowed = unlink_provider_allowed?(provider) + - link_allowed = link_provider_allowed?(provider) + - if unlink_allowed || link_allowed + .provider-btn-group + .provider-btn-image + = provider_image_tag(provider) + - if auth_active?(provider) + - if unlink_allowed + = link_to unlink_profile_account_path(provider: provider), method: :delete, class: 'provider-btn' do + = s_('Profiles|Disconnect') + - else + %a.provider-btn + = s_('Profiles|Active') + - elsif link_allowed + = link_to omniauth_authorize_path(:user, provider), method: :post, class: 'provider-btn not-active' do + = s_('Profiles|Connect') + = render_if_exists 'profiles/accounts/group_saml_unlink_buttons', group_saml_identities: group_saml_identities diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index ee2c5a13b8a..e6380817c8f 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -29,24 +29,7 @@ %p = s_('Profiles|Activate signin with one of the following services') .col-lg-8 - %label.label-bold - = s_('Profiles|Connected Accounts') - %p= s_('Profiles|Click on icon to activate signin with one of the following services') - - button_based_providers.each do |provider| - .provider-btn-group - .provider-btn-image - = provider_image_tag(provider) - - if auth_active?(provider) - - if unlink_allowed?(provider) - = link_to unlink_profile_account_path(provider: provider), method: :delete, class: 'provider-btn' do - = s_('Profiles|Disconnect') - - else - %a.provider-btn - = s_('Profiles|Active') - - else - = link_to omniauth_authorize_path(:user, provider), method: :post, class: 'provider-btn not-active' do - = s_('Profiles|Connect') - = render_if_exists 'profiles/accounts/group_saml_unlink_buttons', group_saml_identities: local_assigns[:group_saml_identities] + = render 'providers', providers: button_based_providers, group_saml_identities: local_assigns[:group_saml_identities] %hr - if current_user.can_change_username? .row.prepend-top-default diff --git a/app/views/profiles/notifications/_email_settings.html.haml b/app/views/profiles/notifications/_email_settings.html.haml new file mode 100644 index 00000000000..34dcf8f5402 --- /dev/null +++ b/app/views/profiles/notifications/_email_settings.html.haml @@ -0,0 +1,6 @@ +- form = local_assigns.fetch(:form) +.form-group + = form.label :notification_email, class: "label-bold" + = form.select :notification_email, @user.all_emails, { include_blank: false }, class: "select2", disabled: local_assigns.fetch(:email_change_disabled, nil) + .help-block + = local_assigns.fetch(:help_text, nil) diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml index 712eb2a4573..e616e5546b3 100644 --- a/app/views/profiles/notifications/show.html.haml +++ b/app/views/profiles/notifications/show.html.haml @@ -22,9 +22,7 @@ Global notification settings = form_for @user, url: profile_notifications_path, method: :put, html: { class: 'update-notifications prepend-top-default' } do |f| - .form-group - = f.label :notification_email, class: "label-bold" - = f.select :notification_email, @user.all_emails, { include_blank: false }, class: "select2" + = render_if_exists 'profiles/notifications/email_settings', form: f = label_tag :global_notification_level, "Global notification level", class: "label-bold" %br diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml index 4d3d92d09c0..1fffea08dae 100644 --- a/app/views/profiles/show.html.haml +++ b/app/views/profiles/show.html.haml @@ -83,18 +83,7 @@ = f.text_field :name, label: 'Full name', required: true, title: s_("Profiles|Using emojis in names seems fun, but please try to set a status message instead"), wrapper: { class: 'col-md-9 qa-full-name' }, help: s_("Profiles|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) - = f.text_field :email, required: true, class: 'input-lg', readonly: true, help: s_("Profiles|Your email address was automatically set based on your %{provider_label} account") % { provider_label: attribute_provider_label(:email) } - - else - = f.text_field :email, required: true, class: 'input-lg', value: (@user.email unless @user.temp_oauth_email?), - help: user_email_help_text(@user) - = f.select :public_email, options_for_select(@user.all_emails, selected: @user.public_email), - { help: s_("Profiles|This email will be displayed on your public profile"), include_blank: s_("Profiles|Do not show on profile") }, - control_class: 'select2 input-lg' - - commit_email_docs_link = link_to s_('Profiles|Learn more'), help_page_path('user/profile/index', anchor: 'commit-email', target: '_blank') - = f.select :commit_email, options_for_select(commit_email_select_options(@user), selected: selected_commit_email(@user)), - { help: s_("Profiles|This email will be used for web based operations, such as edits and merges. %{learn_more}").html_safe % { learn_more: commit_email_docs_link } }, - control_class: 'select2 input-lg' + = render_if_exists 'profiles/email_settings', form: f = f.text_field :skype, class: 'input-md', placeholder: s_("Profiles|username") = f.text_field :linkedin, class: 'input-md', help: s_("Profiles|Your LinkedIn profile name from linkedin.com/in/profilename") = f.text_field :twitter, class: 'input-md', placeholder: s_("Profiles|@username") diff --git a/app/views/projects/mirrors/_authentication_method.html.haml b/app/views/projects/mirrors/_authentication_method.html.haml index 293a2e3ebfe..ef6db07a1bb 100644 --- a/app/views/projects/mirrors/_authentication_method.html.haml +++ b/app/views/projects/mirrors/_authentication_method.html.haml @@ -9,6 +9,7 @@ = f.select :auth_method, options_for_select(auth_options, mirror.auth_method), {}, { class: "form-control js-mirror-auth-type qa-authentication-method" } + = f.hidden_field :auth_method, value: "password", class: "js-hidden-mirror-auth-type" .form-group .collapse.js-well-changing-auth diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml index 55adeb345ab..5d307d6a70d 100644 --- a/app/views/projects/pipelines/_info.html.haml +++ b/app/views/projects/pipelines/_info.html.haml @@ -10,13 +10,7 @@ .icon-container = icon('clock-o') = pluralize @pipeline.total_size, "job" - - if @pipeline.ref - from - - if @pipeline.ref_exists? - = link_to @pipeline.ref, project_ref_path(@project, @pipeline.ref), class: "ref-name" - - else - %span.ref-name - = @pipeline.ref + = @pipeline.ref_text - if @pipeline.duration in = time_interval_in_words(@pipeline.duration) @@ -48,9 +42,9 @@ content: "<a class='autodevops-link' href='#{popover_content_url}' target='_blank' rel='noopener noreferrer nofollow'>#{popover_content_text}</a>", } } Auto DevOps - - if @pipeline.merge_request_event? - %span.js-pipeline-url-mergerequest.badge.badge-info.has-tooltip{ title: "This pipeline is run in a merge request context" } - merge request + - if @pipeline.detached_merge_request_pipeline? + %span.js-pipeline-url-mergerequest.badge.badge-info.has-tooltip{ title: "This pipeline is run on the source branch" } + detached - if @pipeline.stuck? %span.js-pipeline-url-stuck.badge.badge-warning stuck diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml index 6c28cfa31e7..4e4638085fd 100644 --- a/app/views/projects/pipelines/index.html.haml +++ b/app/views/projects/pipelines/index.html.haml @@ -1,6 +1,8 @@ - @no_container = true - page_title _('Pipelines') += render_if_exists "shared/shared_runners_minutes_limit_flash_message" + %div{ 'class' => container_class } #pipelines-list-vue{ data: { endpoint: project_pipelines_path(@project, format: :json), "help-page-path" => help_page_path('ci/quick_start/README'), diff --git a/app/views/search/_category.html.haml b/app/views/search/_category.html.haml index aaf9b973cda..df408e5fb60 100644 --- a/app/views/search/_category.html.haml +++ b/app/views/search/_category.html.haml @@ -1,3 +1,11 @@ +- users = capture_haml do + - if search_tabs?(:members) + %li{ class: active_when(@scope == 'users') } + = link_to search_filter_path(scope: 'users') do + Users + %span.badge.badge-pill + = limited_count(@search_results.limited_users_count) + .scrolling-tabs-container.inner-page-scroll-tabs.is-smaller .fade-left= icon('angle-left') .fade-right= icon('angle-right') @@ -45,6 +53,7 @@ = _("Commits") %span.badge.badge-pill = @search_results.commits_count + = users - elsif @show_snippets %li{ class: active_when(@scope == 'snippet_blobs') } @@ -78,3 +87,4 @@ = _("Milestones") %span.badge.badge-pill = limited_count(@search_results.limited_milestones_count) + = users diff --git a/app/views/search/_results.html.haml b/app/views/search/_results.html.haml index be7a2436d16..2e62039b90a 100644 --- a/app/views/search/_results.html.haml +++ b/app/views/search/_results.html.haml @@ -20,7 +20,7 @@ .search-results - if @scope == 'projects' .term - = render 'shared/projects/list', projects: @search_objects + = render 'shared/projects/list', projects: @search_objects, pipeline_status: false - else = render partial: "search/results/#{@scope.singularize}", collection: @search_objects diff --git a/app/views/search/results/_user.html.haml b/app/views/search/results/_user.html.haml new file mode 100644 index 00000000000..8060a1577e4 --- /dev/null +++ b/app/views/search/results/_user.html.haml @@ -0,0 +1,10 @@ +%ul.content-list + %li + .avatar-cell.d-none.d-sm-block + = user_avatar(user: user, user_name: user.name, css_class: 'd-none d-sm-inline avatar s40') + .user-info + = link_to user_path(user), class: 'd-none d-sm-inline' do + .item-title + = user.name + = user_status(user) + .cgray= user.to_reference diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml index d2b1be29eb9..90fb067e75d 100644 --- a/app/views/shared/projects/_project.html.haml +++ b/app/views/shared/projects/_project.html.haml @@ -10,7 +10,7 @@ - show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true && can_show_last_commit_in_list?(project) - css_class = '' unless local_assigns[:css_class] - css_class += " no-description" if project.description.blank? && !show_last_commit_as_description -- cache_key = project_list_cache_key(project) +- cache_key = project_list_cache_key(project, pipeline_status: pipeline_status) - updated_tooltip = time_ago_with_tooltip(project.last_activity_date) - css_controls_class = compact_mode ? "" : "flex-lg-row justify-content-lg-between" - avatar_container_class = project.creator && use_creator_avatar ? '' : 'rect-avatar' diff --git a/changelogs/unreleased/43297-authorized-application-count.yml b/changelogs/unreleased/43297-authorized-application-count.yml new file mode 100644 index 00000000000..d22e155fb14 --- /dev/null +++ b/changelogs/unreleased/43297-authorized-application-count.yml @@ -0,0 +1,5 @@ +--- +title: Fix authorized application count +merge_request: 25715 +author: moyuru +type: fixed diff --git a/changelogs/unreleased/53139-hide-tree-single-file.yml b/changelogs/unreleased/53139-hide-tree-single-file.yml new file mode 100644 index 00000000000..17fe957e42e --- /dev/null +++ b/changelogs/unreleased/53139-hide-tree-single-file.yml @@ -0,0 +1,5 @@ +--- +title: collapse file tree by default if the merge request changes only one file +merge_request: +author: Riccardo Padovani <riccardo@rpadovani.com> +type: changed diff --git a/changelogs/unreleased/56089-merge-gitlab-keys.yml b/changelogs/unreleased/56089-merge-gitlab-keys.yml new file mode 100644 index 00000000000..5e2cafd3254 --- /dev/null +++ b/changelogs/unreleased/56089-merge-gitlab-keys.yml @@ -0,0 +1,5 @@ +--- +title: Merge the gitlab-shell "gitlab-keys" functionality into GitLab CE +merge_request: 25598 +author: +type: other diff --git a/changelogs/unreleased/58410-change-pixel-size-of-instance-header-footer-message-to-16px.yml b/changelogs/unreleased/58410-change-pixel-size-of-instance-header-footer-message-to-16px.yml new file mode 100644 index 00000000000..3e494847e75 --- /dev/null +++ b/changelogs/unreleased/58410-change-pixel-size-of-instance-header-footer-message-to-16px.yml @@ -0,0 +1,5 @@ +--- +title: Reduce height of instance system header and footer +merge_request: 25752 +author: +type: changed diff --git a/changelogs/unreleased/58883-fix-fetching-comments.yml b/changelogs/unreleased/58883-fix-fetching-comments.yml new file mode 100644 index 00000000000..14c0f1687f2 --- /dev/null +++ b/changelogs/unreleased/58883-fix-fetching-comments.yml @@ -0,0 +1,5 @@ +--- +title: Fix error shown when loading links to specific comments +merge_request: 26092 +author: +type: fixed diff --git a/changelogs/unreleased/58889-spinners-are-active-prematurely-in-bitbucket-cloud-import.yml b/changelogs/unreleased/58889-spinners-are-active-prematurely-in-bitbucket-cloud-import.yml new file mode 100644 index 00000000000..ec357d9a832 --- /dev/null +++ b/changelogs/unreleased/58889-spinners-are-active-prematurely-in-bitbucket-cloud-import.yml @@ -0,0 +1,5 @@ +--- +title: Fix continuous bitbucket import loading spinner +merge_request: 26175 +author: +type: fixed diff --git a/changelogs/unreleased/58999-z-index-issue-on-pipeline-dropdown.yml b/changelogs/unreleased/58999-z-index-issue-on-pipeline-dropdown.yml new file mode 100644 index 00000000000..9a7a0e5af37 --- /dev/null +++ b/changelogs/unreleased/58999-z-index-issue-on-pipeline-dropdown.yml @@ -0,0 +1,6 @@ +--- +title: Fix issue that caused the "Show all activity" button to appear on top of the + mini pipeline status dropdown on the merge request page +merge_request: 26274 +author: +type: fixed diff --git a/changelogs/unreleased/59057-buttons-on-top-from-a-user-profile-page-on-mobile.yml b/changelogs/unreleased/59057-buttons-on-top-from-a-user-profile-page-on-mobile.yml new file mode 100644 index 00000000000..febbbce2139 --- /dev/null +++ b/changelogs/unreleased/59057-buttons-on-top-from-a-user-profile-page-on-mobile.yml @@ -0,0 +1,5 @@ +--- +title: Improve mobile UI on User Profile page +merge_request: 26240 +author: Takuya Noguchi +type: other diff --git a/changelogs/unreleased/59208-fix-error-500-on-every-page-when-active-broadcast-message-present-after-upgrading-to-11-9-0.yml b/changelogs/unreleased/59208-fix-error-500-on-every-page-when-active-broadcast-message-present-after-upgrading-to-11-9-0.yml new file mode 100644 index 00000000000..3c9feae5a04 --- /dev/null +++ b/changelogs/unreleased/59208-fix-error-500-on-every-page-when-active-broadcast-message-present-after-upgrading-to-11-9-0.yml @@ -0,0 +1,6 @@ +--- +title: Gracefully handles excluded fields from attributes during serialization on + JsonCache +merge_request: 26368 +author: +type: fixed diff --git a/changelogs/unreleased/avoid_es_loading_project_ci_status.yml b/changelogs/unreleased/avoid_es_loading_project_ci_status.yml new file mode 100644 index 00000000000..514909c730d --- /dev/null +++ b/changelogs/unreleased/avoid_es_loading_project_ci_status.yml @@ -0,0 +1,5 @@ +--- +title: Avoid loading pipeline status in project search +merge_request: 26342 +author: +type: performance diff --git a/changelogs/unreleased/do-not-force-2fa.yml b/changelogs/unreleased/do-not-force-2fa.yml new file mode 100644 index 00000000000..f9be40e8f37 --- /dev/null +++ b/changelogs/unreleased/do-not-force-2fa.yml @@ -0,0 +1,6 @@ +--- +title: Add link on two-factor authorization settings page to leave group that enforces + two-factor authorization +merge_request: 25731 +author: +type: changed diff --git a/changelogs/unreleased/feature-users-search-results.yml b/changelogs/unreleased/feature-users-search-results.yml new file mode 100644 index 00000000000..151d08bce12 --- /dev/null +++ b/changelogs/unreleased/feature-users-search-results.yml @@ -0,0 +1,5 @@ +--- +title: Add users search results to global search +merge_request: 21197 +author: Alexis Reigel +type: added diff --git a/changelogs/unreleased/fix-projects-partial-locals.yml b/changelogs/unreleased/fix-projects-partial-locals.yml new file mode 100644 index 00000000000..7e2cc008105 --- /dev/null +++ b/changelogs/unreleased/fix-projects-partial-locals.yml @@ -0,0 +1,5 @@ +--- +title: Fix undefined variable error on json project views +merge_request: 26297 +author: +type: fixed diff --git a/changelogs/unreleased/nfriend-update-pipeline-detail-view.yml b/changelogs/unreleased/nfriend-update-pipeline-detail-view.yml new file mode 100644 index 00000000000..a24325c4eb6 --- /dev/null +++ b/changelogs/unreleased/nfriend-update-pipeline-detail-view.yml @@ -0,0 +1,5 @@ +--- +title: Update pipeline detail view to accommodate post-merge pipelines +merge_request: 25775 +author: +type: added diff --git a/changelogs/unreleased/only-counted-active-milestones-as-started.yml b/changelogs/unreleased/only-counted-active-milestones-as-started.yml new file mode 100644 index 00000000000..1a9c4b9023b --- /dev/null +++ b/changelogs/unreleased/only-counted-active-milestones-as-started.yml @@ -0,0 +1,5 @@ +--- +title: Only consider active milestones when using the special Started milestone filter +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/security-2826-fix-project-serialization-in-quick-actions.yml b/changelogs/unreleased/security-2826-fix-project-serialization-in-quick-actions.yml new file mode 100644 index 00000000000..272f8a95957 --- /dev/null +++ b/changelogs/unreleased/security-2826-fix-project-serialization-in-quick-actions.yml @@ -0,0 +1,5 @@ +--- +title: Remove project serialization in quick actions response +merge_request: +author: +type: security diff --git a/changelogs/unreleased/sh-create-branch-as-project-owner-for-github-import.yml b/changelogs/unreleased/sh-create-branch-as-project-owner-for-github-import.yml new file mode 100644 index 00000000000..a3d484cbf05 --- /dev/null +++ b/changelogs/unreleased/sh-create-branch-as-project-owner-for-github-import.yml @@ -0,0 +1,5 @@ +--- +title: 'GitHub import: Create new branches as project owner' +merge_request: 26335 +author: +type: fixed diff --git a/changelogs/unreleased/sh-fix-issue-59065.yml b/changelogs/unreleased/sh-fix-issue-59065.yml new file mode 100644 index 00000000000..41cd5ce0960 --- /dev/null +++ b/changelogs/unreleased/sh-fix-issue-59065.yml @@ -0,0 +1,5 @@ +--- +title: Fix Error 500 when user commits Wiki page with no commit message +merge_request: 26247 +author: +type: fixed diff --git a/changelogs/unreleased/sh-reject-info-refs-head-requests.yml b/changelogs/unreleased/sh-reject-info-refs-head-requests.yml new file mode 100644 index 00000000000..0dca18e2fd8 --- /dev/null +++ b/changelogs/unreleased/sh-reject-info-refs-head-requests.yml @@ -0,0 +1,5 @@ +--- +title: Reject HEAD requests to info/refs endpoint +merge_request: 26334 +author: +type: fixed diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 47c76d8bc49..eba7d2b9fb7 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -697,6 +697,7 @@ production: &base ## GitLab Shell settings gitlab_shell: path: /home/git/gitlab-shell/ + authorized_keys_file: /home/git/.ssh/authorized_keys # File that contains the secret key for verifying access for gitlab-shell. # Default is '.gitlab_shell_secret' relative to Rails.root (i.e. root of the GitLab app). @@ -854,6 +855,7 @@ test: path: tmp/tests/backups gitlab_shell: path: tmp/tests/gitlab-shell/ + authorized_keys_file: tmp/tests/authorized_keys issues_tracker: redmine: title: "Redmine" diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 03800f3d9d2..99bdf5a95c2 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -356,6 +356,7 @@ Settings['sidekiq']['log_format'] ||= 'default' Settings['gitlab_shell'] ||= Settingslogic.new({}) Settings.gitlab_shell['path'] = Settings.absolute(Settings.gitlab_shell['path'] || Settings.gitlab['user_home'] + '/gitlab-shell/') Settings.gitlab_shell['hooks_path'] = :deprecated_use_gitlab_shell_path_instead +Settings.gitlab_shell['authorized_keys_file'] ||= nil Settings.gitlab_shell['secret_file'] ||= Rails.root.join('.gitlab_shell_secret') Settings.gitlab_shell['receive_pack'] = true if Settings.gitlab_shell['receive_pack'].nil? Settings.gitlab_shell['upload_pack'] = true if Settings.gitlab_shell['upload_pack'].nil? diff --git a/config/initializers/sentry.rb b/config/initializers/sentry.rb index abc91c3ae51..680cfa6f0ed 100644 --- a/config/initializers/sentry.rb +++ b/config/initializers/sentry.rb @@ -20,6 +20,21 @@ def configure_sentry # Sanitize authentication headers config.sanitize_http_headers = %w[Authorization Private-Token] config.tags = { program: Gitlab.process_name } + # Debugging for https://gitlab.com/gitlab-org/gitlab-ce/issues/57727 + config.before_send = lambda do |event, hint| + if ActiveModel::MissingAttributeError === hint[:exception] + columns_hash = ActiveRecord::Base + .connection + .schema_cache + .instance_variable_get(:@columns_hash) + .map { |k, v| [k, v.map(&:first)] } + .to_h + + event.extra.merge!(columns_hash) + end + + event + end end end end diff --git a/doc/administration/container_registry.md b/doc/administration/container_registry.md index a1ac4a2a57c..b21bfafc096 100644 --- a/doc/administration/container_registry.md +++ b/doc/administration/container_registry.md @@ -1,6 +1,7 @@ # GitLab Container Registry administration > **Notes:** +> > - [Introduced][ce-4040] in GitLab 8.8. > - Container Registry manifest `v1` support was added in GitLab 8.9 to support > Docker versions earlier than 1.10. diff --git a/doc/administration/high_availability/redis.md b/doc/administration/high_availability/redis.md index a52bc5c3b02..3daebc4d84b 100644 --- a/doc/administration/high_availability/redis.md +++ b/doc/administration/high_availability/redis.md @@ -14,6 +14,7 @@ a hosted cloud solution or you can use the one that comes bundled with Omnibus GitLab packages. > **Notes:** +> > - Redis requires authentication for High Availability. See > [Redis Security](https://redis.io/topics/security) documentation for more > information. We recommend using a combination of a Redis password and tight @@ -55,6 +56,7 @@ components below. ### High Availability with Sentinel > **Notes:** +> > - Starting with GitLab `8.11`, you can configure a list of Redis Sentinel > servers that will monitor a group of Redis servers to provide failover support. > - Starting with GitLab `8.14`, the Omnibus GitLab Enterprise Edition package @@ -231,6 +233,7 @@ Pick the one that suits your needs. This is the section where we install and set up the new Redis instances. > **Notes:** +> > - We assume that you have installed GitLab and all HA components from scratch. If you > already have it installed and running, read how to > [switch from a single-machine installation to Redis HA](#switching-from-an-existing-single-machine-installation-to-redis-ha). diff --git a/doc/administration/job_artifacts.md b/doc/administration/job_artifacts.md index 8522d046a92..e7792106f81 100644 --- a/doc/administration/job_artifacts.md +++ b/doc/administration/job_artifacts.md @@ -1,6 +1,7 @@ # Jobs artifacts administration > **Notes:** +> > - Introduced in GitLab 8.2 and GitLab Runner 0.7.0. > - Starting with GitLab 8.4 and GitLab Runner 1.0, the artifacts archive format changed to `ZIP`. > - Starting with GitLab 8.17, builds are renamed to jobs. @@ -86,6 +87,7 @@ _The artifacts are stored by default in ### Using object storage > **Notes:** +> > - [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/1762) in > [GitLab Premium](https://about.gitlab.com/pricing/) 9.4. > - Since version 9.5, artifacts are [browsable](../user/project/pipelines/job_artifacts.md#browsing-artifacts), diff --git a/doc/administration/monitoring/index.md b/doc/administration/monitoring/index.md index d18dddf09c0..fa0459b24ff 100644 --- a/doc/administration/monitoring/index.md +++ b/doc/administration/monitoring/index.md @@ -7,4 +7,4 @@ Explore our features to monitor your GitLab instance: - [GitHub imports](github_imports.md): Monitor the health and progress of GitLab's GitHub importer with various Prometheus metrics. - [Monitoring uptime](../../user/admin_area/monitoring/health_check.md): Check the server status using the health check endpoint. - [IP whitelists](ip_whitelist.md): Configure GitLab for monitoring endpoints that provide health check information when probed. -- [nginx_status](https://docs.gitlab.com/omnibus/settings/nginx.html#enabling-disabling-nginx_status): Monitor your Nginx server status +- [nginx_status](https://docs.gitlab.com/omnibus/settings/nginx.html#enablingdisabling-nginx_status): Monitor your Nginx server status diff --git a/doc/administration/monitoring/prometheus/index.md b/doc/administration/monitoring/prometheus/index.md index 20d7ef9bb74..f2ac155a694 100644 --- a/doc/administration/monitoring/prometheus/index.md +++ b/doc/administration/monitoring/prometheus/index.md @@ -1,6 +1,7 @@ # Monitoring GitLab with Prometheus > **Notes:** +> > - Prometheus and the various exporters listed in this page are bundled in the > Omnibus GitLab package. Check each exporter's documentation for the timeline > they got added. For installations from source you will have to install them diff --git a/doc/administration/pages/index.md b/doc/administration/pages/index.md index 279ad018aed..288ce376687 100644 --- a/doc/administration/pages/index.md +++ b/doc/administration/pages/index.md @@ -5,6 +5,7 @@ description: 'Learn how to administer GitLab Pages.' # GitLab Pages administration > **Notes:** +> > - [Introduced][ee-80] in GitLab EE 8.3. > - Custom CNAMEs with TLS support were [introduced][ee-173] in GitLab EE 8.5. > - GitLab Pages [were ported][ce-14605] to Community Edition in GitLab 8.17. diff --git a/doc/administration/repository_storage_types.md b/doc/administration/repository_storage_types.md index 40f7c5566ac..25c3d564560 100644 --- a/doc/administration/repository_storage_types.md +++ b/doc/administration/repository_storage_types.md @@ -86,6 +86,11 @@ by another folder with the next 2 characters. They are both stored in a special ### Hashed object pools +CAUTION: **Beta:** +Hashed objects pools are considered beta, and are not ready for production use. +Follow [gitaly#1548](https://gitlab.com/gitlab-org/gitaly/issues/1548) for +updates. + For deduplication of public forks and their parent repository, objects are pooled in an object pool. These object pools are a third repository where shared objects are stored. diff --git a/doc/administration/uploads.md b/doc/administration/uploads.md index 8c0c7a36736..708b59a273b 100644 --- a/doc/administration/uploads.md +++ b/doc/administration/uploads.md @@ -18,7 +18,7 @@ below. >**Notes:** For historical reasons, uploads are stored into a base directory, which by default is `uploads/-/system`. It is strongly discouraged to change this configuration option on an existing GitLab installation. -_The uploads are stored by default in `/var/opt/gitlab/gitlab-rails/uploads/-/system`._ +_The uploads are stored by default in `/var/opt/gitlab/gitlab-rails/uploads`._ 1. To change the storage path for example to `/mnt/storage/uploads`, edit `/etc/gitlab/gitlab.rb` and add the following line: diff --git a/doc/api/group_milestones.md b/doc/api/group_milestones.md index eb974267084..1c2f56581eb 100644 --- a/doc/api/group_milestones.md +++ b/doc/api/group_milestones.md @@ -1,6 +1,5 @@ # Group milestones API -> **Notes:** > [Introduced][ce-12819] in GitLab 9.5. ## List group milestones diff --git a/doc/api/pipeline_schedules.md b/doc/api/pipeline_schedules.md index 2e5b8df9a83..50d9e007ecc 100644 --- a/doc/api/pipeline_schedules.md +++ b/doc/api/pipeline_schedules.md @@ -278,7 +278,7 @@ curl --request DELETE --header "PRIVATE-TOKEN: k5ESFgWY2Qf5xEvDcFxZ" "https://gi } ``` -## Pipeline schedule variable +## Pipeline schedule variables > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/34518) in GitLab 10.0. diff --git a/doc/api/project_snippets.md b/doc/api/project_snippets.md index f02674adfe2..0ccb0517e08 100644 --- a/doc/api/project_snippets.md +++ b/doc/api/project_snippets.md @@ -121,7 +121,6 @@ Parameters: ## Get user agent details -> **Notes:** > [Introduced][ce-29508] in GitLab 9.4. Available only for admins. diff --git a/doc/api/search.md b/doc/api/search.md index 330047e323b..6ee3d32d8bc 100644 --- a/doc/api/search.md +++ b/doc/api/search.md @@ -17,7 +17,7 @@ GET /search | `scope` | string | yes | The scope to search in | | `search` | string | yes | The search query | -Search the expression within the specified scope. Currently these scopes are supported: projects, issues, merge_requests, milestones, snippet_titles, snippet_blobs. +Search the expression within the specified scope. Currently these scopes are supported: projects, issues, merge_requests, milestones, snippet_titles, snippet_blobs, users. The response depends on the requested scope. @@ -281,6 +281,27 @@ Example response: ] ``` +### Scope: users + +```bash +curl --request GET --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/search?scope=users&search=doe +``` + +Example response: + +```json +[ + { + "id": 1, + "name": "John Doe1", + "username": "user1", + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/c922747a93b40d1ea88262bf1aebee62?s=80&d=identicon", + "web_url": "http://localhost/user1" + } +] +``` + ## Group Search API Search within the specified group. @@ -297,7 +318,7 @@ GET /groups/:id/search | `scope` | string | yes | The scope to search in | | `search` | string | yes | The search query | -Search the expression within the specified scope. Currently these scopes are supported: projects, issues, merge_requests, milestones. +Search the expression within the specified scope. Currently these scopes are supported: projects, issues, merge_requests, milestones, users. The response depends on the requested scope. @@ -499,6 +520,27 @@ Example response: ] ``` +### Scope: users + +```bash +curl --request GET --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/groups/3/search?scope=users&search=doe +``` + +Example response: + +```json +[ + { + "id": 1, + "name": "John Doe1", + "username": "user1", + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/c922747a93b40d1ea88262bf1aebee62?s=80&d=identicon", + "web_url": "http://localhost/user1" + } +] +``` + ## Project Search API Search within the specified project. @@ -515,7 +557,7 @@ GET /projects/:id/search | `scope` | string | yes | The scope to search in | | `search` | string | yes | The search query | -Search the expression within the specified scope. Currently these scopes are supported: issues, merge_requests, milestones, notes, wiki_blobs, commits, blobs. +Search the expression within the specified scope. Currently these scopes are supported: issues, merge_requests, milestones, notes, wiki_blobs, commits, blobs, users. The response depends on the requested scope. @@ -828,4 +870,25 @@ Example response: ] ``` +### Scope: users + +```bash +curl --request GET --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/6/search?scope=users&search=doe +``` + +Example response: + +```json +[ + { + "id": 1, + "name": "John Doe1", + "username": "user1", + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/c922747a93b40d1ea88262bf1aebee62?s=80&d=identicon", + "web_url": "http://localhost/user1" + } +] +``` + [ce-41763]: https://gitlab.com/gitlab-org/gitlab-ce/issues/41763 diff --git a/doc/api/services.md b/doc/api/services.md index c44f5cc5781..03d0a80aa64 100644 --- a/doc/api/services.md +++ b/doc/api/services.md @@ -505,10 +505,9 @@ GET /projects/:id/services/jira Set JIRA service for a project. -> **Notes:** -> - Starting with GitLab 8.14, `api_url`, `issues_url`, `new_issue_url` and -> `project_url` are replaced by `project_key`, `url`. If you are using an -> older version, [follow this documentation][old-jira-api]. +> Starting with GitLab 8.14, `api_url`, `issues_url`, `new_issue_url` and +> `project_url` are replaced by `project_key`, `url`. If you are using an +> older version, [follow this documentation][old-jira-api]. ``` PUT /projects/:id/services/jira diff --git a/doc/ci/chatops/README.md b/doc/ci/chatops/README.md index df7fb8a4912..a06fe6961a7 100644 --- a/doc/ci/chatops/README.md +++ b/doc/ci/chatops/README.md @@ -2,9 +2,9 @@ > **Notes:** > -> * [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/4466) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 10.6. [Moved](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/24780) to [GitLab Core](https://about.gitlab.com/pricing/) in 11.9. +> - [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/4466) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 10.6. [Moved](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/24780) to [GitLab Core](https://about.gitlab.com/pricing/) in 11.9. > -> * ChatOps is currently in alpha, with some important features missing like access control. +> - ChatOps is currently in alpha, with some important features missing like access control. GitLab ChatOps provides a method to interact with CI/CD jobs through chat services like Slack. Many organizations' discussion, collaboration, and troubleshooting is taking place in chat services these days, and having a method to run CI/CD jobs with output posted back to the channel can significantly augment a team's workflow. diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md index 4f9efb57b8d..9266c4511be 100644 --- a/doc/ci/docker/using_docker_build.md +++ b/doc/ci/docker/using_docker_build.md @@ -389,6 +389,7 @@ If you're running multiple Runners you will have to modify all configuration fil ## Using the GitLab Container Registry > **Notes:** +> > - This feature requires GitLab 8.8 and GitLab Runner 1.2. > - Starting from GitLab 8.12, if you have [2FA] enabled in your account, you need > to pass a [personal access token][pat] instead of your password in order to diff --git a/doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/index.md b/doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/index.md index 90a8f5917f8..908cf85980e 100644 --- a/doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/index.md +++ b/doc/ci/examples/devops_and_game_dev_with_gitlab_ci_cd/index.md @@ -4,7 +4,7 @@ author_gitlab: blitzgren level: intermediate article_type: tutorial date: 2018-03-07 -last_updated: 2019-03-06 +last_updated: 2019-03-11 --- # DevOps and Game Dev with GitLab CI/CD @@ -21,7 +21,7 @@ and the basics of game development. Our [demo game](http://gitlab-game-demo.s3-website-us-east-1.amazonaws.com/) consists of a simple spaceship traveling in space that shoots by clicking the mouse in a given direction. -Creating a strong CI/CD pipeline at the beginning of developing another game, [Dark Nova](http://darknova.io/about), +Creating a strong CI/CD pipeline at the beginning of developing another game, [Dark Nova](http://darknova.io/), was essential for the fast pace the team worked at. This tutorial will build upon my [previous introductory article](https://ryanhallcs.wordpress.com/2017/03/15/devops-and-game-dev/) and go through the following steps: @@ -38,8 +38,8 @@ This will also provide boilerplate code for starting a browser-based game with the following components: - Written in [Typescript](https://www.typescriptlang.org/) and [PhaserJs](https://phaser.io) -- Building, running, and testing with [Gulp](http://gulpjs.com/) -- Unit tests with [Chai](http://chaijs.com/) and [Mocha](https://mochajs.org/) +- Building, running, and testing with [Gulp](https://gulpjs.com) +- Unit tests with [Chai](https://www.chaijs.com) and [Mocha](https://mochajs.org/) - CI/CD with GitLab - Hosting the codebase on GitLab.com - Hosting the game on AWS @@ -318,7 +318,7 @@ allowing us to run our tests by every push. Our entire `.gitlab-ci.yml` file should now look like this: ```yml -image: node:6 +image: node:10 build: stage: build @@ -413,7 +413,7 @@ use root security credentials. Proper IAM credential management is beyond the sc article, but AWS will remind you that using root credentials is unadvised and against their best practices, as they should. Feel free to follow best practices and use a custom IAM user's credentials, which will be the same two credentials (Key ID and Secret). It's a good idea to -fully understand [IAM Best Practices in AWS](http://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html). We need to add these credentials to GitLab: +fully understand [IAM Best Practices in AWS](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html). We need to add these credentials to GitLab: 1. Log into your AWS account and go to the [Security Credentials page](https://console.aws.amazon.com/iam/home#/security_credential) 1. Click the **Access Keys** section and **Create New Access Key**. Create the key and keep the id and secret around, you'll need them later @@ -455,7 +455,7 @@ Be sure to update the region and S3 URL in that last script command to fit your Our final configuration file `.gitlab-ci.yml` looks like: ```yml -image: node:6 +image: node:10 build: stage: build @@ -503,7 +503,7 @@ deploy: ## Conclusion Within the [demo repository](https://gitlab.com/blitzgren/gitlab-game-demo) you can also find a handful of boilerplate code to get -[Typescript](https://www.typescriptlang.org/), [Mocha](https://mochajs.org/), [Gulp](http://gulpjs.com/) and [Phaser](https://phaser.io) all playing +[Typescript](https://www.typescriptlang.org/), [Mocha](https://mochajs.org/), [Gulp](https://gulpjs.com/) and [Phaser](https://phaser.io) all playing together nicely with GitLab CI/CD, which is the result of lessons learned while making [Dark Nova](http://darknova.io/). Using a combination of free and open source software, we have a full CI/CD pipeline, a game foundation, and unit tests, all running and deployed at every push to master - with shockingly little code. @@ -522,6 +522,6 @@ Here are some ideas to further investigate that can speed up or improve your pip - [Yarn](https://yarnpkg.com) instead of npm - Set up a custom [Docker](../../../ci/docker/using_docker_images.md#define-image-and-services-from-gitlab-ciyml) image that can preload dependencies and tools (like AWS CLI) -- Forward a [custom domain](http://docs.aws.amazon.com/AmazonS3/latest/dev/website-hosting-custom-domain-walkthrough.html) to your game's S3 static website +- Forward a [custom domain](https:/docs.aws.amazon.com/AmazonS3/latest/dev/website-hosting-custom-domain-walkthrough.html) to your game's S3 static website - Combine jobs if you find it unnecessary for a small project - Avoid the queues and set up your own [custom GitLab CI/CD runner](https://about.gitlab.com/2016/03/01/gitlab-runner-with-docker/) diff --git a/doc/ci/merge_request_pipelines/index.md b/doc/ci/merge_request_pipelines/index.md index edb5f8c9ff8..e8953d235a7 100644 --- a/doc/ci/merge_request_pipelines/index.md +++ b/doc/ci/merge_request_pipelines/index.md @@ -13,11 +13,11 @@ for merge requests. ## Configuring pipelines for merge requests -To configure pipelines for merge request, add the `only: merge_requests` parameter to -the jobs that you want it to run only for merge requests. +To configure pipelines for merge requests, add the `only: merge_requests` parameter to +the jobs that you want to run only for merge requests. -Then, when developers create or update merge requests, a pipeline runs on -their new commits at every push to GitLab. +Then, when developers create or update merge requests, a pipeline runs +every time a commit is pushed to GitLab. NOTE: **Note**: If you use this feature with [merge when pipeline succeeds](../../user/project/merge_requests/merge_when_pipeline_succeeds.md), @@ -51,10 +51,10 @@ After the merge request is updated with new commits: - The pipeline fetches the latest code from the source branch and run tests against it. In the above example, the pipeline contains only `build` and `test` jobs. -Since the `deploy` job doesn't have the `only: merge_requests` rule, +Since the `deploy` job doesn't have the `only: merge_requests` parameter, deployment jobs will not happen in the merge request. -Pipelines tagged with **merge request** badge indicate that they were triggered +Pipelines tagged with the **merge request** badge indicate that they were triggered when a merge request was created or updated. For example: ![Merge request page](img/merge_request.png) @@ -65,16 +65,16 @@ The same tag is shown on the pipeline's details: ## Excluding certain jobs -The behavior of the `only: merge_requests` rule is such that _only_ jobs with -that rule are run in the context of a merge request; no other jobs will be run. +The behavior of the `only: merge_requests` parameter is such that _only_ jobs with +that parameter are run in the context of a merge request; no other jobs will be run. -However, you may want to reverse this behaviour, having all of your jobs to run _except_ +However, you may want to reverse this behavior, having all of your jobs to run _except_ for one or two. Consider the following pipeline, with jobs `A`, `B`, and `C`. Imagine you want: -- All pipelines to always run `A` and `B` -- Only want `C` to run for a merge request, +- All pipelines to always run `A` and `B`. +- `C` to run only for merge requests. To achieve this, you can configure your `.gitlab-ci.yml` file as follows: @@ -102,10 +102,10 @@ C: - merge_requests ``` -Because: +Therefore: -- `A` and `B` are getting the `only:` rule to execute in all cases, they will always run. -- `C` specifies that it should only run for merge requests, it will not run for any pipeline +- Since `A` and `B` are getting the `only:` rule to execute in all cases, they will always run. +- Since `C` specifies that it should only run for merge requests, it will not run for any pipeline except a merge request pipeline. As you can see, this will help you avoid a lot of boilerplate where you'd need diff --git a/doc/ci/pipelines.md b/doc/ci/pipelines.md index a77389332c4..c509c341d1e 100644 --- a/doc/ci/pipelines.md +++ b/doc/ci/pipelines.md @@ -4,26 +4,26 @@ ## Introduction -Pipelines are the top-level component of continuous integration, deployment, and delivery. +Pipelines are the top-level component of continuous integration, delivery, and deployment. Pipelines comprise: - Jobs that define what to run. For example, code compilation or test runs. -- Stages that define when and how to run. For example, that test run after code compilation. +- Stages that define when and how to run. For example, that tests run only after code compilation. -Jobs in a stage are executed by [Runners](runners/README.md) in parallel, if there are enough concurrent [Runners](runners/README.md). +Multiple jobs in the same stage are executed by [Runners](runners/README.md) in parallel, if there are enough concurrent [Runners](runners/README.md). -If the jobs in a stage: +If all the jobs in a stage: - Succeed, the pipeline moves on to the next stage. -- Fail, the next stage is not (usually) executed. +- Fail, the next stage is not (usually) executed and the pipeline ends early. NOTE: **Note:** -If you have a [mirrored repository where GitLab pulls from](https://docs.gitlab.com/ee/workflow/repository_mirroring.html#pulling-from-a-remote-repository-starter), +If you have a [mirrored repository that GitLab pulls from](https://docs.gitlab.com/ee/workflow/repository_mirroring.html#pulling-from-a-remote-repository-starter), you may need to enable pipeline triggering in your project's **Settings > Repository > Pull from a remote repository > Trigger pipelines for mirror updates**. -### Simple example +### Simple pipeline example As an example, imagine a pipeline consisting of four stages, executed in the following order: @@ -38,26 +38,26 @@ As an example, imagine a pipeline consisting of four stages, executed in the fol Pipelines can be complex structures with many sequential and parallel jobs. -To make it easier to understand the flow of a pipeline, GitLab has pipeline graphs for viewing pipeline +To make it easier to understand the flow of a pipeline, GitLab has pipeline graphs for viewing pipelines and their statuses. -Pipeline graphs can be displayed in two different ways, depending on what page you -access the graph. +Pipeline graphs can be displayed in two different ways, depending on the page you +access the graph from. NOTE: **Note:** GitLab capitalizes the stages' names when shown in the [pipeline graphs](#pipeline-graphs). ### Regular pipeline graphs -Regular pipeline graphs that show the names of the jobs of each stage. Regular pipeline graphs can +Regular pipeline graphs show the names of the jobs of each stage. Regular pipeline graphs can be found when you are on a [single pipeline page](#seeing-pipeline-status). For example: ![Pipelines example](img/pipelines.png) ### Pipeline mini graphs -Pipeline mini graphs takes less space and can give you a -quick glance if all jobs pass or something failed. The pipeline mini graph can +Pipeline mini graphs take less space and can tell you at a +quick glance if all jobs passed or something failed. The pipeline mini graph can be found when you navigate to: - The pipelines index page. @@ -101,7 +101,7 @@ For example: ### How pipeline duration is calculated Total running time for a given pipeline excludes retries and pending -(queue) time. +(queued) time. Each job is represented as a `Period`, which consists of: @@ -120,7 +120,7 @@ In the example: - B begins at 2 and ends at 4. - C begins at 6 and ends at 7. -Visually it can be viewed as: +Visually, it can be viewed as: ```text 0 1 2 3 4 5 6 7 @@ -144,7 +144,7 @@ In particular: - Jobs are the [basic configuration](yaml/README.html#introduction) component. - Stages are defined using the [`stages`](yaml/README.html#stages) keyword. -For all available configuration options, see [GitLab CI/CD Pipeline Configuration Reference](yaml/README.md). +For all available configuration options, see the [GitLab CI/CD Pipeline Configuration Reference](yaml/README.md). ### Settings and schedules @@ -214,7 +214,7 @@ GitLab supports configuring pipelines that run only for merge requests. For more Pipeline status and test coverage report badges are available and configurable for each project. -For information on adding pipeline badges to project, see [Pipeline badges](../user/project/pipelines/settings.md#pipeline-badges). +For information on adding pipeline badges to projects, see [Pipeline badges](../user/project/pipelines/settings.md#pipeline-badges). ## Multi-project pipelines **[PREMIUM]** @@ -224,7 +224,7 @@ For more information, see [Multi-project pipelines](https://docs.gitlab.com/ee/c ## Working with pipelines -Generally, pipelines are executed automatically and require no intervention once created. +In general, pipelines are executed automatically and require no intervention once created. However, there are instances where you'll need to interact with pipelines. These are documented below. @@ -277,7 +277,7 @@ can quickly check the reason it failed: - In the pipeline widgets, in the merge requests and commit pages. - In the job views, in the global and detailed views of a job. -In any case, if you hover over the failed job you can see the reason it failed. +In each place, if you hover over the failed job you can see the reason it failed. ![Pipeline detail](img/job_failure_reason.png) @@ -294,8 +294,8 @@ allow you to require manual interaction before moving forward in the pipeline. You can do this straight from the pipeline graph. Just click on the play button to execute that particular job. -For example, your entire pipeline could run automatically, but require manual action to -[deploy to production](environments.md#manually-deploying-to-environments). Below, the `production` +For example, your pipeline start automatically, but require manual action to +[deploy to production](environments.md#manually-deploying-to-environments). In the example below, the `production` stage has a job with a manual action. ![Pipelines example](img/pipelines.png) @@ -304,16 +304,16 @@ stage has a job with a manual action. > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/21767) in GitLab 11.4. -When you do not want to run a job immediately, you can use the [`when:deplayed`](yaml/README.md#whendelayed) parameter to +When you do not want to run a job immediately, you can use the [`when:delayed`](yaml/README.md#whendelayed) parameter to delay a job's execution for a certain period. This is especially useful for timed incremental rollout where new code is rolled out gradually. -For example, if you start rolling out new code and users: +For example, if you start rolling out new code and: -- Do not experience trouble, GitLab can automatically complete the deployment from 0% to 100%. -- Experience trouble with the new code, you can stop the timed incremental rollout by canceling the pipeline - and [rolling](environments.md#rolling-back-changes) back to last stable version. +- Users do not experience trouble, GitLab can automatically complete the deployment from 0% to 100%. +- Users experience trouble with the new code, you can stop the timed incremental rollout by canceling the pipeline + and [rolling](environments.md#rolling-back-changes) back to the last stable version. ![Pipelines example](img/pipeline_incremental_rollout.png) @@ -336,14 +336,14 @@ The following actions are allowed on protected branches only if the user is [allowed to merge or push](../user/project/protected_branches.md#using-the-allowed-to-merge-and-allowed-to-push-settings) on that specific branch: -- Run manual pipelines (using [Web UI](#manually-executing-pipelines) or pipelines API). +- Run manual pipelines (using the [Web UI](#manually-executing-pipelines) or pipelines API). - Run scheduled pipelines. - Run pipelines using triggers. - Trigger manual actions on existing pipelines. -- Retry/cancel existing jobs (using Web UI or pipelines API). +- Retry or cancel existing jobs (using the Web UI or pipelines API). **Variables** marked as **protected** are accessible only to jobs that -run on protected branches, avoiding untrusted users to get unintended access to +run on protected branches, preventing untrusted users getting unintended access to sensitive information like deployment credentials and tokens. **Runners** marked as **protected** can run jobs only on protected diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 135de68e200..e1045864db7 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -268,15 +268,9 @@ There are also two edge cases worth mentioning: ### `stage` -NOTE: **Note:** -By default, when using your own Runners, the GitLab Runner installation is set up to run only one job at a time (see the `concurrent` flag in [Runner global settings](https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-global-section) for more information). -Jobs will run in parallel only if: - - Run on different Runners - - The Runner's `concurrent` config has been changed. - `stage` is defined per-job and relies on [`stages`](#stages) which is defined globally. It allows to group jobs into different stages, and jobs of the same -`stage` are executed in `parallel`. For example: +`stage` are executed in parallel (subject to [certain conditions](#using-your-own-runners)). For example: ```yaml stages: @@ -301,6 +295,17 @@ job 4: script: make deploy ``` +#### Using your own Runners + +When using your own Runners, GitLab Runner runs only one job at a time by default (see the +`concurrent` flag in [Runner global settings](https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-global-section) +for more information). + +Jobs will run on your own Runners in parallel only if: + +- Run on different Runners. +- The Runner's `concurrent` setting has been changed. + ### `only`/`except` (basic) `only` and `except` are two parameters that set a job policy to limit when @@ -1235,8 +1240,7 @@ job: to the paths defined in `artifacts:paths`). NOTE: **Note:** -To exclude the folders/files which should not be a part of `untracked` just -add them to `.gitignore`. +`artifacts:untracked` ignores configuration in the repository's `.gitignore` file. Send all Git untracked files: diff --git a/doc/development/README.md b/doc/development/README.md index 13646cbfe48..5a33c46c620 100644 --- a/doc/development/README.md +++ b/doc/development/README.md @@ -54,6 +54,7 @@ description: 'Learn how to contribute to GitLab.' - [Prometheus metrics](prometheus_metrics.md) - [Guidelines for reusing abstractions](reusing_abstractions.md) - [DeclarativePolicy framework](policies.md) +- [How Git object deduplication works in GitLab](git_object_deduplication.md) ## Performance guides diff --git a/doc/development/architecture.md b/doc/development/architecture.md index ae880e3deb6..115c8cfb9ff 100644 --- a/doc/development/architecture.md +++ b/doc/development/architecture.md @@ -158,6 +158,18 @@ TODO TODO +### Registry + +The registry is what users use to store their own Docker images. The bundled +registry uses nginx as a load balancer and GitLab as an authentication manager. +Whenever a client requests to pull or push an image from the registry, it will +return a `401` response along with a header detailing where to get an +authentication token, in this case the GitLab instance. The client will then +request a pull or push auth token from GitLab and retry the original request +to the registry. Learn more about [token authentication](https://docs.docker.com/registry/spec/auth/token/). + +An external registry can also be configured to use GitLab as an auth endpoint. + ## GitLab by Request Type GitLab provides two "interfaces" for end users to access the service: diff --git a/doc/development/git_object_deduplication.md b/doc/development/git_object_deduplication.md new file mode 100644 index 00000000000..81c5f69c7b8 --- /dev/null +++ b/doc/development/git_object_deduplication.md @@ -0,0 +1,261 @@ +# How Git object deduplication works in GitLab + +When a GitLab user [forks a project](../workflow/forking_workflow.md), +GitLab creates a new Project with an associated Git repository that is a +copy of the original project at the time of the fork. If a large project +gets forked often, this can lead to a quick increase in Git repository +storage disk use. To counteract this problem, we are adding Git object +deduplication for forks to GitLab. In this document, we will describe how +GitLab implements Git object deduplication. + +## Enabling Git object deduplication via feature flags + +As of GitLab 11.9, Git object deduplication in GitLab is in beta. In this +document, you can read about the caveats of enabling the feature. Also, +note that Git object deduplication is limited to forks of public +projects on hashed repository storage. + +You can enable deduplication globally by setting the `object_pools` +feature flag to `true`: + +``` {.ruby} +Feature.enable(:object_pools) +``` + +Or just for forks of a specific project: + +``` {.ruby} +fork_parent = Project.find(MY_PROJECT_ID) +Feature.enable(:object_pools, fork_parent) +``` + +To check if a project uses Git object deduplication, look in a Rails +console if `project.pool_repository` is present. + +## Pool repositories + +### Understanding Git alternates + +At the Git level, we achieve deduplication by using [Git +alternates](https://git-scm.com/docs/gitrepository-layout#gitrepository-layout-objects). +Git alternates is a mechanism that lets a repository borrow objects from +another repository on the same machine. + +If we want repository A to borrow from repository B, we first write a +path that resolves to `B.git/objects` in the special file +`A.git/objects/info/alternates`. This establishes the alternates link. +Next, we must perform a Git repack in A. After the repack, any objects +that are duplicated between A and B will get deleted from A. Repository +A is now no longer self-contained, but it still has its own refs and +configuration. Objects in A that are not in B will remain in A. For this +to work, it is of course critical that **no objects ever get deleted from +B** because A might need them. + +### Git alternates in GitLab: pool repositories + +GitLab organizes this object borrowing by creating special **pool +repositories** which are hidden from the user. We then use Git +alternates to let a collection of project repositories borrow from a +single pool repository. We call such a collection of project +repositories a pool. Pools form star-shaped networks of repositories +that borrow from a single pool, which will resemble (but not be +identical to) the fork networks that get formed when users fork +projects. + +At the Git level, pool repositories are created and managed using Gitaly +RPC calls. Just like with normal repositories, the authority on which +pool repositories exist, and which repositories borrow from them, lies +at the Rails application level in SQL. + +In conclusion, we need three things for effective object deduplication +across a collection of GitLab project repositories at the Git level: + +1. A pool repository must exist. +2. The participating project repositories must be linked to the pool + repository via their respective `objects/info/alternates` files. +3. The pool repository must contain Git object data common to the + participating project repositories. + +### Deduplication factor + +The effectiveness of Git object deduplication in GitLab depends on the +amount of overlap between the pool repository and each of its +participants. As of GitLab 11.9, we have a somewhat optimistic system. +The only data that will be deduplicated is the data in the source +project repository at the time the pool repository is created. That is, +the data in the source project at the time of the first fork *after* the +deduplication feature has been enabled. + +When we enable the object deduplication feature for +gitlab.com/gitlab-org/gitlab-ce, which is about 1GB at the time of +writing, all new forks of that project would be 1GB smaller than they +would have been without Git object deduplication. So even in its current +optimistic form, we expect Git object deduplication in GitLab to make a +difference. + +However, if a lot of Git objects get added to the project repositories +in a pool after the pool repository was created these new Git objects +will currently (GitLab 11.9) not get deduplicated. Over time, the +deduplication factor of the pool will get worse and worse. + +As an extreme example, if we create an empty repository A, and fork that +to repository B, behind the scenes we get an object pool P with no +objects in it at all. If we then push 1GB of Git data to A, and push the +same Git data to B, it will not get deduplicated, because that data was +not in A at the time P was created. + +This also matters in less extreme examples. Consider a pool P with +source project A and 500 active forks B1, B2,...,B500. Suppose, +optimistically, that the forks are fully deduplicated at the start of +our scenario. Now some time passes and 200MB of new Git data gets added +to project A. Because of the forking workflow, this data makes also its way +into the forks B1, ..., B500. That means we would now have 100GB of Git +data sitting around (500 \* 200MB) across the forks, that could have +been deduplicated. But because of the way we do deduplication this new +data will not be deduplicated. + +> TODO Add periodic maintenance of object pools to prevent gradual loss +> of deduplication over time. +> https://gitlab.com/groups/gitlab-org/-/epics/524 + +## SQL model + +As of GitLab 11.8, project repositories in GitLab do not have their own +SQL table. They are indirectly identified by columns on the `projects` +table. In other words, the only way to look up a project repository is to +first look up its project, and then call `project.repository`. + +With pool repositories we made a fresh start. These live in their own +`pool_repositories` SQL table. The relations between these two tables +are as follows: + +- a `Project` belongs to at most one `PoolRepository` + (`project.pool_repository`) +- as an automatic consequence of the above, a `PoolRepository` has + many `Project`s +- a `PoolRepository` has exactly one "source `Project`" + (`pool.source_project`) + +### Assumptions + +- All repositories in a pool must use [hashed + storage](../administration/repository_storage_types.md). This is so + that we don't have to ever worry about updating paths in + `object/info/alternates` files. +- All repositories in a pool must be on the same Gitaly storage shard. + The Git alternates mechanism relies on direct disk access across + multiple repositories, and we can only assume direct disk access to + be possible within a Gitaly storage shard. +- All project repositories in a pool must have "Public" visibility in + GitLab at the time they join. There are gotchas around visibility of + Git objects across alternates links. This restriction is a defense + against accidentally leaking private Git data. +- The only two ways to remove a member project from a pool are (1) to + delete the project or (2) to move the project to another Gitaly + storage shard. + +### Creating pools and pool memberships + +- When a pool gets created, it must have a source project. The initial + contents of the pool repository are a Git clone of the source + project repository. +- The occasion for creating a pool is when an existing eligible + (public, hashed storage, non-forked) GitLab project gets forked and + this project does not belong to a pool repository yet. The fork + parent project becomes the source project of the new pool, and both + the fork parent and the fork child project become members of the new + pool. +- Once project A has become the source project of a pool, all future + eligible forks of A will become pool members. +- If the fork source is itself a fork, the resulting repository will + neither join the repository nor will a new pool repository be + seeded. + + eg: + + Suppose fork A is part of a pool repository, any forks created off + of fork A *will not* be a part of the pool repository that fork A is + a part of. + + Suppose B is a fork of A, and A does not belong to an object pool. + Now C gets created as a fork of B. C will not be part of a pool + repository. + +> TODO should forks of forks be deduplicated? +> https://gitlab.com/gitlab-org/gitaly/issues/1532 + +### Consequences + +- If a normal Project participating in a pool gets moved to another + Gitaly storage shard, its "belongs to PoolRepository" relation must + be broken. Because of the way moving repositories between shard is + implemented, we will automatically get a fresh self-contained copy + of the project's repository on the new storage shard. +- If the source project of a pool gets moved to another Gitaly storage + shard or is deleted, we may have to break the "PoolRepository has + one source Project" relation? + +> TODO What happens, or should happen, if a source project changes +> visibility, is deleted, or moves to another storage shard? +> https://gitlab.com/gitlab-org/gitaly/issues/1488 + +## Consistency between the SQL pool relation and Gitaly + +As far as Gitaly is concerned, the SQL pool relations make two types of +claims about the state of affairs on the Gitaly server: pool repository +existence, and the existence of an alternates connection between a +repository and a pool. + +### Pool existence + +If GitLab thinks a pool repository exists (i.e. it exists according to +SQL), but it does not on the Gitaly server, then certain RPC calls that +take the object pool as an argument will fail. + +> TODO What happens if SQL says the pool repo exists but Gitaly says it +> does not? https://gitlab.com/gitlab-org/gitaly/issues/1533 + +If GitLab thinks a pool does not exist, while it does exist on disk, +that has no direct consequences on its own. However, if other +repositories on disk borrow objects from this unknown pool repository +then we risk data loss, see below. + +### Pool relation existence + +There are three different things that can go wrong here. + +#### 1. SQL says repo A belongs to pool P but Gitaly says A has no alternate objects + +In this case, we miss out on disk space savings but all RPC's on A itself +will function fine. As long as Git can find all its objects, it does not +matter exactly where those objects are. + +#### 2. SQL says repo A belongs to pool P1 but Gitaly says A has alternate objects in pool P2 + +If we are not careful, this situation can lead to data loss. During some +operations (repository maintenance), GitLab will try to re-link A to its +pool P1. If this clobbers the existing link to P2, then A will loose Git +objects and become invalid. + +Also, keep in mind that if GitLab's database got messed up, it may not +even know that P2 exists. + +> TODO Ensure that Gitaly will not clobber existing, unexpected +> alternates links. https://gitlab.com/gitlab-org/gitaly/issues/1534 + +#### 3. SQL says repo A does not belong to any pool but Gitaly says A belongs to P + +This has the same data loss possibility as scenario 2 above. + +## Git object deduplication and GitLab Geo + +When a pool repository record is created in SQL on a Geo primary, this +will eventually trigger an event on the Geo secondary. The Geo secondary +will then create the pool repository in Gitaly. This leads to an +"eventually consistent" situation because as each pool participant gets +synchronized, Geo will eventuall trigger garbage collection in Gitaly on +the secondary, at which stage Git objects will get deduplicated. + +> TODO How do we handle the edge case where at the time the Geo +> secondary tries to create the pool repository, the source project does +> not exist? https://gitlab.com/gitlab-org/gitaly/issues/1533 diff --git a/doc/development/gitaly.md b/doc/development/gitaly.md index b1785e6f803..1a4b1743a00 100644 --- a/doc/development/gitaly.md +++ b/doc/development/gitaly.md @@ -3,6 +3,12 @@ [Gitaly](https://gitlab.com/gitlab-org/gitaly) is a high-level Git RPC service used by GitLab CE/EE, Workhorse and GitLab-Shell. +## Beginner's guide + +Start by reading the gitaly repository's +[Beginner's guide to Gitaly contributions](https://gitlab.com/gitlab-org/gitaly/blob/master/doc/beginners_guide.md). +It describes how to setup gitaly, the various components of gitaly and what they do, and how to run its test suites. + ## Developing new Git features To read or write Git data, a request has to be made to Gitaly. This means that diff --git a/doc/development/go_guide/index.md b/doc/development/go_guide/index.md index cdc806a2d31..7fd41c5e01f 100644 --- a/doc/development/go_guide/index.md +++ b/doc/development/go_guide/index.md @@ -161,12 +161,39 @@ in the code. ### Logging -The usage of a logging library is strongly recommended for daemons. Even though -there is a `log` package in the standard library, we generally use -[logrus](https://github.com/sirupsen/logrus). Its plugin ("hooks") system +The usage of a logging library is strongly recommended for daemons. Even +though there is a `log` package in the standard library, we generally use +[Logrus](https://github.com/sirupsen/logrus). Its plugin ("hooks") system makes it a powerful logging library, with the ability to add notifiers and formatters at the logger level directly. +#### Structured (JSON) logging + +Every binary ideally must have structured (JSON) logging in place as it helps +with searching and filtering the logs. At GitLab we use structured logging in +JSON format, as all our infrastructure assumes that. When using +[Logrus](https://github.com/sirupsen/logrus) you can turn on structured +logging simply by using the build in [JSON +formatter](https://github.com/sirupsen/logrus#formatters). This follows the +same logging type we use in our [Ruby +applications](../logging.md#use-structured-json-logging). + +#### How to use Logrus + +There are a few guidelines one should follow when using the +[Logrus](https://github.com/sirupsen/logrus) package: + +- When printing an error use + [WithError](https://godoc.org/github.com/sirupsen/logrus#WithError). For + exmaple, `logrus.WithError(err).Error("Failed to do something")`. +- Since we use [structured logging](#structured-json-logging) we can log + fields in the context of that code path, such as the URI of the request using + [`WithField`](https://godoc.org/github.com/sirupsen/logrus#WithField) or + [`WithFields`](https://godoc.org/github.com/sirupsen/logrus#WithFields). For + example, `logrus.WithField("file", "/app/go).Info("Opening dir")`. If you + have to log multiple keys, always use `WithFields` instead of calling + `WithField` more than once. + ### Tracing and Correlation [LabKit](https://gitlab.com/gitlab-org/labkit) is a place to keep common diff --git a/doc/development/new_fe_guide/style/html.md b/doc/development/new_fe_guide/style/html.md index 035fcbb28df..e8c9c2ccebf 100644 --- a/doc/development/new_fe_guide/style/html.md +++ b/doc/development/new_fe_guide/style/html.md @@ -2,11 +2,11 @@ ## Buttons -<a name="button-type"></a><a name="1.1"></a> +### Button type -- [1.1](#button-type) **Use button type** Button tags requires a `type` attribute according to the [W3C HTML specification][button-type-spec]. +Button tags requires a `type` attribute according to the [W3C HTML specification](https://www.w3.org/TR/2011/WD-html5-20110525/the-button-element.html#dom-button-type). -``` +```html // bad <button></button> @@ -14,11 +14,11 @@ <button type="button"></button> ``` -<a name="button-role"></a><a name="1.2"></a> +### Button role -- [1.2](#button-role) **Use button role for non buttons** If an HTML element has an onClick handler but is not a button, it should have `role="button"`. This is more [accessible][button-role-accessible]. +If an HTML element has an `onClick` handler but is not a button, it should have `role="button"`. This is [more accessible](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_button_role). -``` +```html // bad <div onClick="doSomething"></div> @@ -28,11 +28,11 @@ ## Links -<a name="blank-links"></a><a name="2.1"></a> +### Blank target -- [2.1](#blank-links) **Use rel for target blank** Use `rel="noopener noreferrer"` whenever your links open in a new window i.e. `target="_blank"`. This prevents [the following][jitbit-target-blank] security vulnerability documented by JitBit +Use `rel="noopener noreferrer"` whenever your links open in a new window, i.e. `target="_blank"`. This prevents a security vulnerability [documented by JitBit](https://www.jitbit.com/alexblog/256-targetblank---the-most-underestimated-vulnerability-ever/). -``` +```html // bad <a href="url" target="_blank"></a> @@ -40,18 +40,14 @@ <a href="url" target="_blank" rel="noopener noreferrer"></a> ``` -<a name="fake-links"></a><a name="2.2"></a> +### Fake links -- [2.2](#fake-links) **Do not use fake links** Use a button tag if a link only invokes JavaScript click event handlers. This is more semantic. +**Do not use fake links.** Use a button tag if a link only invokes JavaScript click event handlers, which is more semantic. -``` +```html // bad <a class="js-do-something" href="#"></a> // good <button class="js-do-something" type="button"></button> ``` - -[button-type-spec]: https://www.w3.org/TR/2011/WD-html5-20110525/the-button-element.html#dom-button-type -[button-role-accessible]: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_button_role -[jitbit-target-blank]: https://www.jitbit.com/alexblog/256-targetblank---the-most-underestimated-vulnerability-ever/ diff --git a/doc/development/testing_guide/end_to_end_tests.md b/doc/development/testing_guide/end_to_end_tests.md index daa0714aec3..7010250b33c 100644 --- a/doc/development/testing_guide/end_to_end_tests.md +++ b/doc/development/testing_guide/end_to_end_tests.md @@ -17,11 +17,13 @@ a black-box testing framework for the API and the UI. We run scheduled pipeline each night to test nightly builds created by Omnibus. You can find these nightly pipelines at [gitlab-org/quality/nightly/pipelines][quality-nightly-pipelines]. +Results are reported in the `#qa-nightly` Slack channel. ### Testing staging We run scheduled pipeline each night to test staging. You can find these nightly pipelines at [gitlab-org/quality/staging/pipelines][quality-staging-pipelines]. +Results are reported in the `#qa-staging` Slack channel. ### Testing code in merge requests @@ -40,6 +42,29 @@ Below you can read more about how to use it and how does it work. Currently, we are using _multi-project pipeline_-like approach to run QA pipelines. +![QA on merge requests CI/CD architecture](img/qa_on_merge_requests_cicd_architecture.png) + +<details> +<summary>Show mermaid source</summary> +<pre> +graph LR + A1 -.->|1. Triggers an omnibus-gitlab pipeline and wait for it to be done| A2 + B2[<b>`Trigger-qa` stage</b><br />`Trigger:qa-test` job] -.->|2. Triggers a gitlab-qa pipeline and wait for it to be done| A3 + +subgraph gitlab-ce/ee pipeline + A1[<b>`test` stage</b><br />`package-and-qa` job] + end + +subgraph omnibus-gitlab pipeline + A2[<b>`Trigger-docker` stage</b></b><br />`Trigger:gitlab-docker` job] -->|once done| B2 + end + +subgraph gitlab-qa pipeline + A3>QA jobs run] -.->|3. Reports back the pipeline result to the `package-and-qa` job<br />and post the result on the original commit tested| A1 + end +</pre> +</details> + 1. Developer triggers a manual action, that can be found in CE / EE merge requests. This starts a chain of pipelines in multiple projects. diff --git a/doc/development/testing_guide/frontend_testing.md b/doc/development/testing_guide/frontend_testing.md index d4a2ac246c4..71c9637e72c 100644 --- a/doc/development/testing_guide/frontend_testing.md +++ b/doc/development/testing_guide/frontend_testing.md @@ -53,12 +53,9 @@ Remember that the performance of each test depends on the environment. ## Karma test suite GitLab uses the [Karma][karma] test runner with [Jasmine] as its test -framework for our JavaScript unit and integration tests. For integration tests, -we generate HTML files using RSpec (see `spec/javascripts/fixtures/*.rb` for examples). -Some fixtures are still HAML templates that are translated to HTML files using the same mechanism (see `static_fixtures.rb`). -Adding these static fixtures should be avoided as they are harder to keep up to date with real views. -The existing static fixtures will be migrated over time. -Please see [gitlab-org/gitlab-ce#24753](https://gitlab.com/gitlab-org/gitlab-ce/issues/24753) to track our progress. +framework for our JavaScript unit and integration tests. +We generate HTML and JSON fixtures from backend views and controllers +using RSpec (see `spec/javascripts/fixtures/*.rb` for examples). Fixtures are served during testing by the [jasmine-jquery][jasmine-jquery] plugin. JavaScript tests live in `spec/javascripts/`, matching the folder structure @@ -228,14 +225,12 @@ See this [section][vue-test]. ### Running frontend tests -`rake karma` runs the frontend-only (JavaScript) tests. -It consists of two subtasks: +For running the frontend tests, you need the following commands: -- `rake karma:fixtures` (re-)generates fixtures -- `rake karma:tests` actually executes the tests +- `rake karma:fixtures` (re-)generates fixtures. +- `yarn test` executes the tests. -As long as the fixtures don't change, `rake karma:tests` (or `yarn karma`) -is sufficient (and saves you some time). +As long as the fixtures don't change, `yarn test` is sufficient (and saves you some time). ### Live testing and focused testing diff --git a/doc/development/testing_guide/img/qa_on_merge_requests_cicd_architecture.png b/doc/development/testing_guide/img/qa_on_merge_requests_cicd_architecture.png Binary files differnew file mode 100644 index 00000000000..5b93a05db96 --- /dev/null +++ b/doc/development/testing_guide/img/qa_on_merge_requests_cicd_architecture.png diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index c78cfc41678..574ba961cb0 100644 --- a/doc/raketasks/backup_restore.md +++ b/doc/raketasks/backup_restore.md @@ -409,6 +409,8 @@ an access key from the Google console first: 1. Select "Interoperability" and create an access key 1. Make note of the "Access Key" and "Secret" and replace them in the configurations below +1. In the buckets advanced settings ensure the Access Control option "Set object-level + and bucket-level permissions" is selected 1. Make sure you already have a bucket created For Omnibus GitLab packages: diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md index c73e810545e..43aa4163ab1 100644 --- a/doc/topics/autodevops/index.md +++ b/doc/topics/autodevops/index.md @@ -230,8 +230,25 @@ can enable/disable Auto DevOps at either the project-level or instance-level. 1. Click **Save changes** for the changes to take effect. NOTE: **Note:** -Even when disabled at the instance level, project maintainers are still able to enable -Auto DevOps at the project level. +Even when disabled at the instance level, group owners and project maintainers are still able to enable +Auto DevOps at group-level and project-level, respectively. + +### Enabling/disabling Auto DevOps at the group-level + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/52447) in GitLab 11.10. + +To enable or disable Auto DevOps at the group-level: + +1. Go to group's **Settings > CI/CD > Auto DevOps** page. +1. Toggle the **Default to Auto DevOps pipeline** checkbox (checked to enable, unchecked to disable). +1. Click **Save changes** button for the changes to take effect. + +When enabling or disabling Auto DevOps at group-level, group configuration will be implicitly used for +the subgroups and projects inside that group, unless Auto DevOps is specifically enabled or disabled on +the subgroup or project. + +NOTE: **Note** +Only administrators and group owners are allowed to enable or disable Auto DevOps at group-level. ### Enabling/disabling Auto DevOps at the project-level diff --git a/doc/university/README.md b/doc/university/README.md index 3e7d02770e4..cf13246067f 100644 --- a/doc/university/README.md +++ b/doc/university/README.md @@ -4,12 +4,9 @@ comments: false # GitLab University -GitLab University is the best place to learn about **Version Control with Git and GitLab**. +GitLab University is a great place to start when learning about version control with Git and GitLab, as well as other GitLab features. -It doesn't replace, but accompanies our great [Documentation](https://docs.gitlab.com) -and [Blog Articles](https://about.gitlab.com/blog/). - -Would you like to contribute to GitLab University? Then please take a look at our contribution [process](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/PROCESS.md) for more information. +If you're looking for a GitLab subscription for _your university_, see our [Education](https://about.gitlab.com/solutions/education/) page. ## GitLab University Curriculum diff --git a/doc/user/discussions/index.md b/doc/user/discussions/index.md index a15e1a59921..cf0dc51ea23 100644 --- a/doc/user/discussions/index.md +++ b/doc/user/discussions/index.md @@ -19,13 +19,14 @@ comment at any time, and anyone with [Maintainer access level][permissions] or higher can also edit a comment made by someone else. You can also reply to a comment notification email to reply to the comment if -[Reply by email] is configured for your GitLab instance. Replying to a standard comment +[Reply by email] is configured for your GitLab instance. Replying to a standard comment creates another standard comment. Replying to a discussion comment creates a reply in the discussion thread. Email replies support [Markdown] and [quick actions], just as if you replied from the web. ## Resolvable comments and discussions > **Notes:** +> > - The main feature was [introduced][ce-5022] in GitLab 8.11. > - Resolvable discussions can be added only to merge request diffs. @@ -357,7 +358,7 @@ Clicking on the **Reply to comment** button will bring the reply area into focus ![Reply to comment feature](img/reply_to_comment.gif) -Relying to a non-discussion comment will convert the non-discussion comment to a +Relying to a non-discussion comment will convert the non-discussion comment to a threaded discussion once the reply is submitted. This conversion is considered an edit to the original comment, so a note about when it was last edited will appear underneath it. diff --git a/doc/user/group/clusters/index.md b/doc/user/group/clusters/index.md index 6d67688fdff..e67795b9bae 100644 --- a/doc/user/group/clusters/index.md +++ b/doc/user/group/clusters/index.md @@ -25,8 +25,22 @@ deployments. | Application | GitLab version | Description | Helm Chart | | ----------- | -------------- | ----------- | ---------- | -| [Helm Tiller](https://docs.helm.sh) | 10.2+ | Helm is a package manager for Kubernetes and is required to install all the other applications. It is installed in its own pod inside the cluster which can run the `helm` CLI in a safe environment. | n/a | -| [Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress) | 10.2+ | Ingress can provide load balancing, SSL termination, and name-based virtual hosting. It acts as a web proxy for your applications and is useful if you want to use [Auto DevOps](../../../topics/autodevops/index.md) or deploy your own web apps. | [stable/nginx-ingress](https://github.com/helm/charts/tree/master/stable/nginx-ingress) | +| [Helm Tiller](https://docs.helm.sh) | 11.6+ | Helm is a package manager for Kubernetes and is required to install all the other applications. It is installed in its own pod inside the cluster which can run the `helm` CLI in a safe environment. | n/a | +| [Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress) | 11.6+ | Ingress can provide load balancing, SSL termination, and name-based virtual hosting. It acts as a web proxy for your applications and is useful if you want to use [Auto DevOps](../../../topics/autodevops/index.md) or deploy your own web apps. | [stable/nginx-ingress](https://github.com/helm/charts/tree/master/stable/nginx-ingress) | +| [Cert-Manager](https://docs.cert-manager.io/en/latest/) | 11.6+ | Cert-Manager is a native Kubernetes certificate management controller that helps with issuing certificates. Installing Cert-Manager on your cluster will issue a certificate by [Let's Encrypt](https://letsencrypt.org/) and ensure that certificates are valid and up-to-date. | [stable/cert-manager](https://github.com/helm/charts/tree/master/stable/cert-manager) | + +NOTE: **Note:** +Some [cluster +applications](../../project/clusters/index.md#installing-applications) +are installable only for a project-level cluster. Support for installing these +applications in a group-level cluster is planned for future releases. For updates, see: + +- Support installing [Runner in group-level + clusters](https://gitlab.com/gitlab-org/gitlab-ce/issues/51988) +- Support installing [JupyterHub in group-level + clusters](https://gitlab.com/gitlab-org/gitlab-ce/issues/51989) +- Support installing [Prometheus in group-level + clusters](https://gitlab.com/gitlab-org/gitlab-ce/issues/51963) ## RBAC compatibility diff --git a/doc/user/group/index.md b/doc/user/group/index.md index 1fe8017adbc..3bcfd30079d 100644 --- a/doc/user/group/index.md +++ b/doc/user/group/index.md @@ -63,9 +63,8 @@ together in a single list view. ## Create a new group -> **Notes:** -> - For a list of words that are not allowed to be used as group names see the -> [reserved names](../reserved_names.md). +> For a list of words that are not allowed to be used as group names see the +> [reserved names](../reserved_names.md). You can create a group in GitLab from: diff --git a/doc/user/profile/account/two_factor_authentication.md b/doc/user/profile/account/two_factor_authentication.md index 8e5fe4b0fb9..b74bd81d467 100644 --- a/doc/user/profile/account/two_factor_authentication.md +++ b/doc/user/profile/account/two_factor_authentication.md @@ -59,6 +59,7 @@ of recovery codes. ### Enable 2FA via U2F device > **Notes:** +> > - GitLab officially only supports [Yubikey] U2F devices. > - Support for U2F devices was added in GitLab 8.8. diff --git a/doc/user/project/bulk_editing.md b/doc/user/project/bulk_editing.md index fead99c5e88..d0c7daf4692 100644 --- a/doc/user/project/bulk_editing.md +++ b/doc/user/project/bulk_editing.md @@ -1,6 +1,7 @@ # Bulk editing issues and merge requests > **Notes:** +> > - A permission level of `Reporter` or higher is required in order to manage > issues. > - A permission level of `Developer` or higher is required in order to manage diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md index d1206b0c80a..b05e8777b40 100644 --- a/doc/user/project/clusters/index.md +++ b/doc/user/project/clusters/index.md @@ -358,7 +358,7 @@ by GitLab before installing any of the applications. | ----------- | :------------: | ----------- | --------------- | | [Helm Tiller](https://docs.helm.sh/) | 10.2+ | Helm is a package manager for Kubernetes and is required to install all the other applications. It is installed in its own pod inside the cluster which can run the `helm` CLI in a safe environment. | n/a | | [Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/) | 10.2+ | Ingress can provide load balancing, SSL termination, and name-based virtual hosting. It acts as a web proxy for your applications and is useful if you want to use [Auto DevOps] or deploy your own web apps. | [stable/nginx-ingress](https://github.com/helm/charts/tree/master/stable/nginx-ingress) | -| [Cert Manager](http://docs.cert-manager.io/en/latest/) | 11.6+ | Cert Manager is a native Kubernetes certificate management controller that helps with issuing certificates. Installing Cert Manager on your cluster will issue a certificate by [Let's Encrypt](https://letsencrypt.org/) and ensure that certificates are valid and up-to-date. | [stable/cert-manager](https://github.com/helm/charts/tree/master/stable/cert-manager) | +| [Cert-Manager](https://docs.cert-manager.io/en/latest/) | 11.6+ | Cert-Manager is a native Kubernetes certificate management controller that helps with issuing certificates. Installing Cert-Manager on your cluster will issue a certificate by [Let's Encrypt](https://letsencrypt.org/) and ensure that certificates are valid and up-to-date. | [stable/cert-manager](https://github.com/helm/charts/tree/master/stable/cert-manager) | | [Prometheus](https://prometheus.io/docs/introduction/overview/) | 10.4+ | Prometheus is an open-source monitoring and alerting system useful to supervise your deployed applications. | [stable/prometheus](https://github.com/helm/charts/tree/master/stable/prometheus) | | [GitLab Runner](https://docs.gitlab.com/runner/) | 10.6+ | GitLab Runner is the open source project that is used to run your jobs and send the results back to GitLab. It is used in conjunction with [GitLab CI/CD](https://about.gitlab.com/features/gitlab-ci-cd/), the open-source continuous integration service included with GitLab that coordinates the jobs. When installing the GitLab Runner via the applications, it will run in **privileged mode** by default. Make sure you read the [security implications](#security-implications) before doing so. | [runner/gitlab-runner](https://gitlab.com/charts/gitlab-runner) | | [JupyterHub](http://jupyter.org/) | 11.0+ | [JupyterHub](https://jupyterhub.readthedocs.io/en/stable/) is a multi-user service for managing notebooks across a team. [Jupyter Notebooks](https://jupyter-notebook.readthedocs.io/en/latest/) provide a web-based interactive programming environment used for data analysis, visualization, and machine learning. We use a [custom Jupyter image](https://gitlab.com/gitlab-org/jupyterhub-user-image/blob/master/Dockerfile) that installs additional useful packages on top of the base Jupyter. Authentication will be enabled only for [project members](../members/index.md) with [Developer or higher](../../permissions.md) access to the project. You will also see ready-to-use DevOps Runbooks built with Nurtch's [Rubix library](https://github.com/amit1rrr/rubix). More information on creating executable runbooks can be found in [our Nurtch documentation](runbooks/index.md#nurtch-executable-runbooks). | [jupyter/jupyterhub](https://jupyterhub.github.io/helm-chart/) | diff --git a/doc/user/project/container_registry.md b/doc/user/project/container_registry.md index dec6eac2508..83b268db967 100644 --- a/doc/user/project/container_registry.md +++ b/doc/user/project/container_registry.md @@ -1,7 +1,8 @@ # GitLab Container Registry > **Notes:** -> [Introduced][ce-4040] in GitLab 8.8. +> +> - [Introduced][ce-4040] in GitLab 8.8. > - Docker Registry manifest `v1` support was added in GitLab 8.9 to support Docker > versions earlier than 1.10. > - This document is about the user guide. To learn how to enable GitLab Container @@ -10,7 +11,7 @@ > - Starting from GitLab 8.12, if you have 2FA enabled in your account, you need > to pass a [personal access token][pat] instead of your password in order to > login to GitLab's Container Registry. -> - Multiple level image names support was added in GitLab 9.1 +> - Multiple level image names support was added in GitLab 9.1. With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images. @@ -41,6 +42,7 @@ to enable it. ## Build and push images > **Notes:** +> > - Moving or renaming existing container registry repositories is not supported > once you have pushed images because the images are signed, and the > signature includes the repository name. diff --git a/doc/user/project/integrations/jira.md b/doc/user/project/integrations/jira.md index 754711f5919..a90167b9767 100644 --- a/doc/user/project/integrations/jira.md +++ b/doc/user/project/integrations/jira.md @@ -56,6 +56,7 @@ When connecting to **JIRA Cloud**, which supports authentication via API token, ### Configuring GitLab > **Notes:** +> > - The currently supported Jira versions are `v6.x` and `v7.x.`. GitLab 7.8 or > higher is required. > - GitLab 8.14 introduced a new way to integrate with Jira which greatly simplified @@ -142,6 +143,7 @@ the same goal: where `PROJECT-1` is the issue ID of the Jira project. > **Notes:** +> > - Only commits and merges into the project's default branch (usually **master**) will > close an issue in Jira. You can change your projects default branch under > [project settings](img/jira_project_settings.png). diff --git a/doc/user/project/merge_requests/versions.md b/doc/user/project/merge_requests/versions.md index 90500fd9c21..70bd1e60594 100644 --- a/doc/user/project/merge_requests/versions.md +++ b/doc/user/project/merge_requests/versions.md @@ -1,6 +1,7 @@ # Merge requests versions > **Notes:** +> > - [Introduced][ce-5467] in GitLab 8.12. > - Comments are disabled while viewing outdated merge versions or comparing to > versions other than base. diff --git a/doc/user/project/milestones/index.md b/doc/user/project/milestones/index.md index e6033ca8655..a7d6144e3ec 100644 --- a/doc/user/project/milestones/index.md +++ b/doc/user/project/milestones/index.md @@ -92,7 +92,7 @@ When filtering by milestone, in addition to choosing a specific project mileston - **None**: Show issues or merge requests with no assigned milestone. - **Any**: Show issues or merge requests that have an assigned milestone. - **Upcoming**: Show issues or merge requests that have been assigned the open milestone that has the next upcoming due date (i.e. nearest due date in the future). -- **Started**: Show issues or merge requests that have an assigned milestone with a start date that is before today. +- **Started**: Show issues or merge requests that have an open assigned milestone with a start date that is before today. ## Milestone view diff --git a/doc/user/project/new_ci_build_permissions_model.md b/doc/user/project/new_ci_build_permissions_model.md index d41b65f7985..6c3fa5eb463 100644 --- a/doc/user/project/new_ci_build_permissions_model.md +++ b/doc/user/project/new_ci_build_permissions_model.md @@ -205,6 +205,7 @@ With the update permission model we also extended the support for accessing Container Registries for private projects. > **Notes:** +> > - GitLab Runner versions prior to 1.8 don't incorporate the introduced changes > for permissions. This makes the `image:` directive to not work with private > projects automatically and it needs to be configured manually on Runner's host diff --git a/doc/user/project/pages/introduction.md b/doc/user/project/pages/introduction.md index f67ef1e6a46..ccb0300e23e 100644 --- a/doc/user/project/pages/introduction.md +++ b/doc/user/project/pages/introduction.md @@ -1,6 +1,7 @@ # Exploring GitLab Pages > **Notes:** +> > - This feature was [introduced][ee-80] in GitLab EE 8.3. > - Custom CNAMEs with TLS support were [introduced][ee-173] in GitLab EE 8.5. > - GitLab Pages [was ported][ce-14605] to Community Edition in GitLab 8.17. diff --git a/doc/user/project/pipelines/job_artifacts.md b/doc/user/project/pipelines/job_artifacts.md index 5271c76fc24..a3f40c20192 100644 --- a/doc/user/project/pipelines/job_artifacts.md +++ b/doc/user/project/pipelines/job_artifacts.md @@ -1,6 +1,7 @@ # Introduction to job artifacts > **Notes:** +> > - Since GitLab 8.2 and GitLab Runner 0.7.0, job artifacts that are created by > GitLab Runner are uploaded to GitLab and are downloadable as a single archive > (`tar.gz`) using the GitLab UI. @@ -152,7 +153,7 @@ For example: https://gitlab.com/gitlab-org/gitlab-ce/-/jobs/artifacts/master/browse?job=coverage ``` -There is also a URL to specific files, including html files that +There is also a URL to specific files, including html files that are shown in [GitLab Pages](../../../administration/pages/index.md): ``` @@ -191,9 +192,9 @@ artifacts and the job's trace. 1. Click the trash icon at the top right of the job's trace. 1. Confirm the deletion. -## Retrieve artifacts of private projects when using GitLab CI +## Retrieve artifacts of private projects when using GitLab CI In order to retrieve a job artifact of a different project, you might need to use a private token in order to [authenticate and download](../../../api/jobs.md#get-job-artifacts) the artifacts. [expiry date]: ../../../ci/yaml/README.md#artifactsexpire_in -[ce-14399]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14399
\ No newline at end of file +[ce-14399]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14399 diff --git a/doc/user/project/pipelines/schedules.md b/doc/user/project/pipelines/schedules.md index 58a0fbc97cd..07ce4f3f5da 100644 --- a/doc/user/project/pipelines/schedules.md +++ b/doc/user/project/pipelines/schedules.md @@ -1,6 +1,7 @@ # Pipeline schedules > **Notes**: +> > - This feature was introduced in 9.1 as [Trigger Schedule][ce-10533]. > - In 9.2, the feature was [renamed to Pipeline Schedule][ce-10853]. > - Cron notation is parsed by [Fugit](https://github.com/floraison/fugit). diff --git a/lib/api/helpers/search_helpers.rb b/lib/api/helpers/search_helpers.rb index 47fb5a36327..0e052e0e273 100644 --- a/lib/api/helpers/search_helpers.rb +++ b/lib/api/helpers/search_helpers.rb @@ -5,17 +5,17 @@ module API module SearchHelpers def self.global_search_scopes # This is a separate method so that EE can redefine it. - %w(projects issues merge_requests milestones snippet_titles snippet_blobs) + %w(projects issues merge_requests milestones snippet_titles snippet_blobs users) end def self.group_search_scopes # This is a separate method so that EE can redefine it. - %w(projects issues merge_requests milestones) + %w(projects issues merge_requests milestones users) end def self.project_search_scopes # This is a separate method so that EE can redefine it. - %w(issues merge_requests milestones notes wiki_blobs commits blobs) + %w(issues merge_requests milestones notes wiki_blobs commits blobs users) end end end diff --git a/lib/api/search.rb b/lib/api/search.rb index f65e810bf90..60095300ea1 100644 --- a/lib/api/search.rb +++ b/lib/api/search.rb @@ -17,7 +17,8 @@ module API blobs: Entities::Blob, wiki_blobs: Entities::Blob, snippet_titles: Entities::Snippet, - snippet_blobs: Entities::Snippet + snippet_blobs: Entities::Snippet, + users: Entities::UserBasic }.freeze def search(additional_params = {}) @@ -51,6 +52,12 @@ module API # Defining this method here as a noop allows us to easily extend it in # EE, without having to modify this file directly. end + + def check_users_search_allowed! + if params[:scope].to_sym == :users && Feature.disabled?(:users_search, default_enabled: true) + render_api_error!({ error: _("Scope not supported with disabled 'users_search' feature!") }, 400) + end + end end resource :search do @@ -67,6 +74,7 @@ module API end get do verify_search_scope! + check_users_search_allowed! present search, with: entity end @@ -87,6 +95,7 @@ module API end get ':id/(-/)search' do verify_search_scope! + check_users_search_allowed! present search(group_id: user_group.id), with: entity end @@ -106,6 +115,8 @@ module API use :pagination end get ':id/(-/)search' do + check_users_search_allowed! + present search(project_id: user_project.id), with: entity end end diff --git a/lib/backup/uploads.rb b/lib/backup/uploads.rb index 9577df2634a..5a20b6ae0a6 100644 --- a/lib/backup/uploads.rb +++ b/lib/backup/uploads.rb @@ -9,7 +9,7 @@ module Backup def initialize(progress) @progress = progress - super('uploads', Rails.root.join('public/uploads')) + super('uploads', File.join(Gitlab.config.uploads.storage_path, "uploads")) end end end diff --git a/lib/gitlab/authorized_keys.rb b/lib/gitlab/authorized_keys.rb new file mode 100644 index 00000000000..609d2bd9c77 --- /dev/null +++ b/lib/gitlab/authorized_keys.rb @@ -0,0 +1,145 @@ +# frozen_string_literal: true + +module Gitlab + class AuthorizedKeys + KeyError = Class.new(StandardError) + + attr_reader :logger + + # Initializes the class + # + # @param [Gitlab::Logger] logger + def initialize(logger = Gitlab::AppLogger) + @logger = logger + end + + # Add id and its key to the authorized_keys file + # + # @param [String] id identifier of key prefixed by `key-` + # @param [String] key public key to be added + # @return [Boolean] + def add_key(id, key) + lock do + public_key = strip(key) + logger.info("Adding key (#{id}): #{public_key}") + open_authorized_keys_file('a') { |file| file.puts(key_line(id, public_key)) } + end + + true + end + + # Atomically add all the keys to the authorized_keys file + # + # @param [Array<::Key>] keys list of Key objects to be added + # @return [Boolean] + def batch_add_keys(keys) + lock(300) do # Allow 300 seconds (5 minutes) for batch_add_keys + open_authorized_keys_file('a') do |file| + keys.each do |key| + public_key = strip(key.key) + logger.info("Adding key (#{key.shell_id}): #{public_key}") + file.puts(key_line(key.shell_id, public_key)) + end + end + end + + true + rescue Gitlab::AuthorizedKeys::KeyError + false + end + + # Remove key by ID from the authorized_keys file + # + # @param [String] id identifier of the key to be removed prefixed by `key-` + # @return [Boolean] + def rm_key(id) + lock do + logger.info("Removing key (#{id})") + open_authorized_keys_file('r+') do |f| + while line = f.gets + next unless line.start_with?("command=\"#{command(id)}\"") + + f.seek(-line.length, IO::SEEK_CUR) + # Overwrite the line with #'s. Because the 'line' variable contains + # a terminating '\n', we write line.length - 1 '#' characters. + f.write('#' * (line.length - 1)) + end + end + end + + true + end + + # Clear the authorized_keys file + # + # @return [Boolean] + def clear + open_authorized_keys_file('w') { |file| file.puts '# Managed by gitlab-rails' } + + true + end + + # Read the authorized_keys file and return IDs of each key + # + # @return [Array<Integer>] + def list_key_ids + logger.info('Listing all key IDs') + + [].tap do |a| + open_authorized_keys_file('r') do |f| + f.each_line do |line| + key_id = line.match(/key-(\d+)/) + + next unless key_id + + a << key_id[1].chomp.to_i + end + end + end + end + + private + + def lock(timeout = 10) + File.open("#{authorized_keys_file}.lock", "w+") do |f| + f.flock File::LOCK_EX + Timeout.timeout(timeout) { yield } + ensure + f.flock File::LOCK_UN + end + end + + def open_authorized_keys_file(mode) + File.open(authorized_keys_file, mode, 0o600) do |file| + file.chmod(0o600) + yield file + end + end + + def key_line(id, key) + key = key.chomp + + if key.include?("\n") || key.include?("\t") + raise KeyError, "Invalid public_key: #{key.inspect}" + end + + %Q(command="#{command(id)}",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty #{strip(key)}) + end + + def command(id) + unless /\A[a-z0-9-]+\z/ =~ id + raise KeyError, "Invalid ID: #{id.inspect}" + end + + "#{File.join(Gitlab.config.gitlab_shell.path, 'bin', 'gitlab-shell')} #{id}" + end + + def strip(key) + key.split(/[ ]+/)[0, 2].join(' ') + end + + def authorized_keys_file + Gitlab.config.gitlab_shell.authorized_keys_file + end + end +end diff --git a/lib/gitlab/ci/model.rb b/lib/gitlab/ci/model.rb index fbdb84c0522..1625cb841b6 100644 --- a/lib/gitlab/ci/model.rb +++ b/lib/gitlab/ci/model.rb @@ -8,7 +8,7 @@ module Gitlab end def model_name - @model_name ||= ActiveModel::Name.new(self, nil, self.name.split("::").last) + @model_name ||= ActiveModel::Name.new(self, nil, self.name.demodulize) end end end diff --git a/lib/gitlab/ci/pipeline/chain/command.rb b/lib/gitlab/ci/pipeline/chain/command.rb index 7b77e86feae..bf9f03f6134 100644 --- a/lib/gitlab/ci/pipeline/chain/command.rb +++ b/lib/gitlab/ci/pipeline/chain/command.rb @@ -11,7 +11,7 @@ module Gitlab :trigger_request, :schedule, :merge_request, :ignore_skip_ci, :save_incompleted, :seeds_block, :variables_attributes, :push_options, - :chat_data + :chat_data, :allow_mirror_update ) do include Gitlab::Utils::StrongMemoize diff --git a/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml index 4e708f229cd..ef6d7866e85 100644 --- a/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml @@ -21,20 +21,19 @@ dast: allow_failure: true services: - docker:stable-dind - before_script: + script: - export DAST_VERSION=${SP_VERSION:-$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')} - | function dast_run() { docker run \ - --env DAST_TARGET_AVAILABILITY_TIMEOUT \ - --volume "$PWD:/output" \ - --volume /var/run/docker.sock:/var/run/docker.sock \ - -w /output \ - "registry.gitlab.com/gitlab-org/security-products/dast:$DAST_VERSION" \ - /analyze -t $DAST_WEBSITE \ - "$@" + --env DAST_TARGET_AVAILABILITY_TIMEOUT \ + --volume "$PWD:/output" \ + --volume /var/run/docker.sock:/var/run/docker.sock \ + -w /output \ + "registry.gitlab.com/gitlab-org/security-products/dast:$DAST_VERSION" \ + /analyze -t $DAST_WEBSITE \ + "$@" } - script: - | if [ -n "$DAST_AUTH_URL" ] then diff --git a/lib/gitlab/diff/suggestion_diff.rb b/lib/gitlab/diff/suggestion_diff.rb new file mode 100644 index 00000000000..ee153c226b7 --- /dev/null +++ b/lib/gitlab/diff/suggestion_diff.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module Gitlab + module Diff + class SuggestionDiff + include Gitlab::Utils::StrongMemoize + + delegate :from_content, :to_content, :from_line, to: :@suggestible + + def initialize(suggestible) + @suggestible = suggestible + end + + def diff_lines + Gitlab::Diff::Parser.new.parse(raw_diff.each_line).to_a + end + + private + + def raw_diff + "#{diff_header}\n#{from_content_as_diff}#{to_content_as_diff}" + end + + def diff_header + "@@ -#{from_line} +#{from_line}" + end + + def from_content_as_diff + from_content.lines.map { |line| line.prepend('-') }.join + end + + def to_content_as_diff + to_content.lines.map { |line| line.prepend('+') }.join + end + end + end +end diff --git a/lib/gitlab/fake_application_settings.rb b/lib/gitlab/fake_application_settings.rb index bd806269bf0..77f7d9490f3 100644 --- a/lib/gitlab/fake_application_settings.rb +++ b/lib/gitlab/fake_application_settings.rb @@ -7,6 +7,8 @@ # column type without parsing db/schema.rb. module Gitlab class FakeApplicationSettings < OpenStruct + include ApplicationSettingImplementation + # Mimic ActiveRecord predicate methods for boolean values def self.define_predicate_methods(options) options.each do |key, value| @@ -26,20 +28,7 @@ module Gitlab FakeApplicationSettings.define_predicate_methods(options) end - def key_restriction_for(type) - 0 - end - - def allowed_key_types - ApplicationSetting::SUPPORTED_KEY_TYPES - end - - def pick_repository_storage - repository_storages.sample - end - - def commit_email_hostname - super.presence || ApplicationSetting.default_commit_email_hostname - end + alias_method :read_attribute, :[] + alias_method :has_attribute?, :[] end end diff --git a/lib/gitlab/favicon.rb b/lib/gitlab/favicon.rb index 1ae2f9dfd93..6e31064f737 100644 --- a/lib/gitlab/favicon.rb +++ b/lib/gitlab/favicon.rb @@ -10,7 +10,7 @@ module Gitlab elsif Gitlab::Utils.to_boolean(ENV['CANARY']) 'favicon-yellow.png' elsif Rails.env.development? - 'favicon-blue.png' + development_favicon else 'favicon.png' end @@ -18,6 +18,12 @@ module Gitlab ActionController::Base.helpers.image_path(image_name, host: host) end + def development_favicon + # This is a separate method so that EE can return a different favicon + # for development environments. + 'favicon-blue.png' + end + def status_overlay(status_name) path = File.join( 'ci_favicons', diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb index e5bbd500e98..88ff9fbceb4 100644 --- a/lib/gitlab/git/commit.rb +++ b/lib/gitlab/git/commit.rb @@ -184,11 +184,12 @@ module Gitlab end end - def initialize(repository, raw_commit, head = nil) + def initialize(repository, raw_commit, head = nil, lazy_load_parents: false) raise "Nil as raw commit passed" unless raw_commit @repository = repository @head = head + @lazy_load_parents = lazy_load_parents init_commit(raw_commit) end @@ -225,6 +226,12 @@ module Gitlab author_name != committer_name || author_email != committer_email end + def parent_ids + return @parent_ids unless @lazy_load_parents + + @parent_ids ||= @repository.commit(id).parent_ids + end + def parent_id parent_ids.first end diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 35dd042ba6a..7d6851a4b8d 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -344,12 +344,12 @@ module Gitlab end end - def new_blobs(newrev) + def new_blobs(newrev, dynamic_timeout: nil) return [] if newrev.blank? || newrev == ::Gitlab::Git::BLANK_SHA strong_memoize("new_blobs_#{newrev}") do wrapped_gitaly_errors do - gitaly_ref_client.list_new_blobs(newrev, REV_LIST_COMMIT_LIMIT) + gitaly_ref_client.list_new_blobs(newrev, REV_LIST_COMMIT_LIMIT, dynamic_timeout: dynamic_timeout) end end end diff --git a/lib/gitlab/gitaly_client/ref_service.rb b/lib/gitlab/gitaly_client/ref_service.rb index d5633d167ac..6f6698607d9 100644 --- a/lib/gitlab/gitaly_client/ref_service.rb +++ b/lib/gitlab/gitaly_client/ref_service.rb @@ -84,15 +84,22 @@ module Gitlab commits end - def list_new_blobs(newrev, limit = 0) + def list_new_blobs(newrev, limit = 0, dynamic_timeout: nil) request = Gitaly::ListNewBlobsRequest.new( repository: @gitaly_repo, commit_id: newrev, limit: limit ) + timeout = + if dynamic_timeout + [dynamic_timeout, GitalyClient.medium_timeout].min + else + GitalyClient.medium_timeout + end + response = GitalyClient - .call(@storage, :ref_service, :list_new_blobs, request, timeout: GitalyClient.medium_timeout) + .call(@storage, :ref_service, :list_new_blobs, request, timeout: timeout) response.flat_map do |msg| # Returns an Array of Gitaly::NewBlobObject objects diff --git a/lib/gitlab/github_import/importer/pull_request_importer.rb b/lib/gitlab/github_import/importer/pull_request_importer.rb index e294173f992..72451e5e01e 100644 --- a/lib/gitlab/github_import/importer/pull_request_importer.rb +++ b/lib/gitlab/github_import/importer/pull_request_importer.rb @@ -89,7 +89,7 @@ module Gitlab return if project.repository.branch_exists?(source_branch) - project.repository.add_branch(merge_request.author, source_branch, pull_request.source_branch_sha) + project.repository.add_branch(project.owner, source_branch, pull_request.source_branch_sha) rescue Gitlab::Git::CommandError => e Gitlab::Sentry.track_acceptable_exception(e, extra: { diff --git a/lib/gitlab/group_search_results.rb b/lib/gitlab/group_search_results.rb new file mode 100644 index 00000000000..7255293b194 --- /dev/null +++ b/lib/gitlab/group_search_results.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Gitlab + class GroupSearchResults < SearchResults + def initialize(current_user, limit_projects, group, query, default_project_filter: false, per_page: 20) + super(current_user, limit_projects, query, default_project_filter: default_project_filter, per_page: per_page) + + @group = group + end + + # rubocop:disable CodeReuse/ActiveRecord + def users + # 1: get all groups the current user has access to + groups = GroupsFinder.new(current_user).execute.joins(:users) + + # 2: Get the group's whole hierarchy + group_users = @group.direct_and_indirect_users + + # 3: get all users the current user has access to (-> + # `SearchResults#users`), which also applies the query. + users = super + + # 4: filter for users that belong to the previously selected groups + users + .where(id: group_users.select('id')) + .where(id: groups.select('members.user_id')) + end + # rubocop:enable CodeReuse/ActiveRecord + end +end diff --git a/lib/gitlab/hashed_storage/migrator.rb b/lib/gitlab/hashed_storage/migrator.rb index f5368d41629..1f0deebea39 100644 --- a/lib/gitlab/hashed_storage/migrator.rb +++ b/lib/gitlab/hashed_storage/migrator.rb @@ -97,7 +97,7 @@ module Gitlab def any_non_empty_queue?(*workers) workers.any? do |worker| - worker.jobs.any? + !Sidekiq::Queue.new(worker.queue).size.zero? end end diff --git a/lib/gitlab/hook_data/issue_builder.rb b/lib/gitlab/hook_data/issue_builder.rb index c99353b9d49..d39ff8c21cc 100644 --- a/lib/gitlab/hook_data/issue_builder.rb +++ b/lib/gitlab/hook_data/issue_builder.rb @@ -48,7 +48,7 @@ module Gitlab } issue.attributes.with_indifferent_access.slice(*self.class.safe_hook_attributes) - .merge!(attrs) + .merge!(attrs) end end end diff --git a/lib/gitlab/hook_data/merge_request_builder.rb b/lib/gitlab/hook_data/merge_request_builder.rb index ad38e26e40a..d77b1d04644 100644 --- a/lib/gitlab/hook_data/merge_request_builder.rb +++ b/lib/gitlab/hook_data/merge_request_builder.rb @@ -55,7 +55,7 @@ module Gitlab } merge_request.attributes.with_indifferent_access.slice(*self.class.safe_hook_attributes) - .merge!(attrs) + .merge!(attrs) end end end diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml index fa54fc17d95..a0aab9fcbaf 100644 --- a/lib/gitlab/import_export/import_export.yml +++ b/lib/gitlab/import_export/import_export.yml @@ -117,6 +117,7 @@ excluded_attributes: - :description_html - :repository_languages - :bfg_object_map + - :tag_list namespaces: - :runners_token - :runners_token_encrypted diff --git a/lib/gitlab/json_cache.rb b/lib/gitlab/json_cache.rb index 24daad638f4..e4bc437d787 100644 --- a/lib/gitlab/json_cache.rb +++ b/lib/gitlab/json_cache.rb @@ -80,8 +80,23 @@ module Gitlab # when the new_record? method incorrectly returns false. # # See https://gitlab.com/gitlab-org/gitlab-ee/issues/9903#note_145329964 - attributes = klass.attributes_builder.build_from_database(raw, {}) - klass.allocate.init_with("attributes" => attributes, "new_record" => new_record?(raw, klass)) + klass + .allocate + .init_with( + "attributes" => attributes_for(klass, raw), + "new_record" => new_record?(raw, klass) + ) + end + + def attributes_for(klass, raw) + # We have models that leave out some fields from the JSON export for + # security reasons, e.g. models that include the CacheMarkdownField. + # The ActiveRecord::AttributeSet we build from raw does know about + # these columns so we need manually set them. + missing_attributes = (klass.columns.map(&:name) - raw.keys) + missing_attributes.each { |column| raw[column] = nil } + + klass.attributes_builder.build_from_database(raw, {}) end def new_record?(raw, klass) diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb index a68f8801c2a..58f06b6708c 100644 --- a/lib/gitlab/project_search_results.rb +++ b/lib/gitlab/project_search_results.rb @@ -22,11 +22,17 @@ module Gitlab paginated_blobs(wiki_blobs, page) when 'commits' Kaminari.paginate_array(commits).page(page).per(per_page) + when 'users' + users.page(page).per(per_page) else super(scope, page, false) end end + def users + super.where(id: @project.team.members) # rubocop:disable CodeReuse/ActiveRecord + end + def blobs_count @blobs_count ||= blobs.count end diff --git a/lib/gitlab/search_results.rb b/lib/gitlab/search_results.rb index 491148ec1a6..8988b9ad7be 100644 --- a/lib/gitlab/search_results.rb +++ b/lib/gitlab/search_results.rb @@ -32,6 +32,8 @@ module Gitlab merge_requests.page(page).per(per_page) when 'milestones' milestones.page(page).per(per_page) + when 'users' + users.page(page).per(per_page) else Kaminari.paginate_array([]).page(page).per(per_page) end @@ -71,6 +73,12 @@ module Gitlab end # rubocop: enable CodeReuse/ActiveRecord + # rubocop:disable CodeReuse/ActiveRecord + def limited_users_count + @limited_users_count ||= users.limit(count_limit).count + end + # rubocop:enable CodeReuse/ActiveRecord + def single_commit_result? false end @@ -79,6 +87,12 @@ module Gitlab 1001 end + def users + return User.none unless Ability.allowed?(current_user, :read_users_list) + + UsersFinder.new(current_user, search: query).execute + end + private def projects diff --git a/lib/gitlab/shell.rb b/lib/gitlab/shell.rb index 40b641b8317..93182607616 100644 --- a/lib/gitlab/shell.rb +++ b/lib/gitlab/shell.rb @@ -10,18 +10,6 @@ module Gitlab Error = Class.new(StandardError) - KeyAdder = Struct.new(:io) do - def add_key(id, key) - key = Gitlab::Shell.strip_key(key) - # Newline and tab are part of the 'protocol' used to transmit id+key to the other end - if key.include?("\t") || key.include?("\n") - raise Error.new("Invalid key: #{key.inspect}") - end - - io.puts("#{id}\t#{key}") - end - end - class << self def secret_token @secret_token ||= begin @@ -40,10 +28,6 @@ module Gitlab .join('GITLAB_SHELL_VERSION')).strip end - def strip_key(key) - key.split(/[ ]+/)[0, 2].join(' ') - end - private # Create (if necessary) and link the secret token file @@ -173,7 +157,7 @@ module Gitlab false end - # Add new key to gitlab-shell + # Add new key to authorized_keys # # Ex. # add_key("key-42", "sha-rsa ...") @@ -181,33 +165,53 @@ module Gitlab def add_key(key_id, key_content) return unless self.authorized_keys_enabled? - gitlab_shell_fast_execute([gitlab_shell_keys_path, - 'add-key', key_id, self.class.strip_key(key_content)]) + if shell_out_for_gitlab_keys? + gitlab_shell_fast_execute([ + gitlab_shell_keys_path, + 'add-key', + key_id, + strip_key(key_content) + ]) + else + gitlab_authorized_keys.add_key(key_id, key_content) + end end # Batch-add keys to authorized_keys # # Ex. - # batch_add_keys { |adder| adder.add_key("key-42", "sha-rsa ...") } - def batch_add_keys(&block) + # batch_add_keys(Key.all) + def batch_add_keys(keys) return unless self.authorized_keys_enabled? - IO.popen(%W(#{gitlab_shell_path}/bin/gitlab-keys batch-add-keys), 'w') do |io| - yield(KeyAdder.new(io)) + if shell_out_for_gitlab_keys? + begin + IO.popen("#{gitlab_shell_keys_path} batch-add-keys", 'w') do |io| + add_keys_to_io(keys, io) + end + + $?.success? + rescue Error + false + end + else + gitlab_authorized_keys.batch_add_keys(keys) end end - # Remove ssh key from gitlab shell + # Remove ssh key from authorized_keys # # Ex. - # remove_key("key-342", "sha-rsa ...") + # remove_key("key-342") # - def remove_key(key_id, key_content = nil) + def remove_key(id, _ = nil) return unless self.authorized_keys_enabled? - args = [gitlab_shell_keys_path, 'rm-key', key_id] - args << key_content if key_content - gitlab_shell_fast_execute(args) + if shell_out_for_gitlab_keys? + gitlab_shell_fast_execute([gitlab_shell_keys_path, 'rm-key', id]) + else + gitlab_authorized_keys.rm_key(id) + end end # Remove all ssh keys from gitlab shell @@ -218,7 +222,11 @@ module Gitlab def remove_all_keys return unless self.authorized_keys_enabled? - gitlab_shell_fast_execute([gitlab_shell_keys_path, 'clear']) + if shell_out_for_gitlab_keys? + gitlab_shell_fast_execute([gitlab_shell_keys_path, 'clear']) + else + gitlab_authorized_keys.clear + end end # Remove ssh keys from gitlab shell that are not in the DB @@ -247,33 +255,6 @@ module Gitlab end # rubocop: enable CodeReuse/ActiveRecord - # Iterate over all ssh key IDs from gitlab shell, in batches - # - # Ex. - # batch_read_key_ids { |batch| keys = Key.where(id: batch) } - # - def batch_read_key_ids(batch_size: 100, &block) - return unless self.authorized_keys_enabled? - - list_key_ids do |key_id_stream| - key_id_stream.lazy.each_slice(batch_size) do |lines| - key_ids = lines.map { |l| l.chomp.to_i } - yield(key_ids) - end - end - end - - # Stream all ssh key IDs from gitlab shell, separated by newlines - # - # Ex. - # list_key_ids - # - def list_key_ids(&block) - return unless self.authorized_keys_enabled? - - IO.popen(%W(#{gitlab_shell_path}/bin/gitlab-keys list-key-ids), &block) - end - # Add empty directory for storing repositories # # Ex. @@ -378,6 +359,10 @@ module Gitlab private + def shell_out_for_gitlab_keys? + Gitlab.config.gitlab_shell.authorized_keys_file.blank? + end + def gitlab_shell_fast_execute(cmd) output, status = gitlab_shell_fast_execute_helper(cmd) @@ -415,6 +400,40 @@ module Gitlab raise Error, e end + def gitlab_authorized_keys + @gitlab_authorized_keys ||= Gitlab::AuthorizedKeys.new + end + + def batch_read_key_ids(batch_size: 100, &block) + return unless self.authorized_keys_enabled? + + if shell_out_for_gitlab_keys? + IO.popen("#{gitlab_shell_keys_path} list-key-ids") do |key_id_stream| + key_id_stream.lazy.each_slice(batch_size) do |lines| + yield(lines.map { |l| l.chomp.to_i }) + end + end + else + gitlab_authorized_keys.list_key_ids.lazy.each_slice(batch_size) do |key_ids| + yield(key_ids) + end + end + end + + def strip_key(key) + key.split(/[ ]+/)[0, 2].join(' ') + end + + def add_keys_to_io(keys, io) + keys.each do |k| + key = strip_key(k.key) + + raise Error.new("Invalid key: #{key.inspect}") if key.include?("\t") || key.include?("\n") + + io.puts("#{k.shell_id}\t#{key}") + end + end + class GitalyGitlabProjects attr_reader :shard_name, :repository_relative_path, :output, :gl_project_path diff --git a/lib/gitlab/sidekiq_config.rb b/lib/gitlab/sidekiq_config.rb index 3b8de64913b..fb303e3fb0c 100644 --- a/lib/gitlab/sidekiq_config.rb +++ b/lib/gitlab/sidekiq_config.rb @@ -48,7 +48,9 @@ module Gitlab end def self.workers - @workers ||= find_workers(Rails.root.join('app', 'workers')) + @workers ||= + find_workers(Rails.root.join('app', 'workers')) + + find_workers(Rails.root.join('ee', 'app', 'workers')) end def self.find_workers(root) diff --git a/lib/gitlab/user_extractor.rb b/lib/gitlab/user_extractor.rb index b41d085ee77..f0557f6ad68 100644 --- a/lib/gitlab/user_extractor.rb +++ b/lib/gitlab/user_extractor.rb @@ -11,7 +11,9 @@ module Gitlab USERNAME_REGEXP = User.reference_pattern def initialize(text) - @text = text + # EE passes an Array to `text` in a few places, so we want to support both + # here. + @text = Array(text).join(' ') end def users diff --git a/lib/gitlab/utils.rb b/lib/gitlab/utils.rb index 99fa65e0e90..16ec8a8bb28 100644 --- a/lib/gitlab/utils.rb +++ b/lib/gitlab/utils.rb @@ -104,6 +104,12 @@ module Gitlab nil end + def try_megabytes_to_bytes(size) + Integer(size).megabytes + rescue ArgumentError + size + end + def bytes_to_megabytes(bytes) bytes.to_f / Numeric::MEGABYTE end diff --git a/lib/tasks/gitlab/shell.rake b/lib/tasks/gitlab/shell.rake index 0ebc6f00793..ee3ef9dad6e 100644 --- a/lib/tasks/gitlab/shell.rake +++ b/lib/tasks/gitlab/shell.rake @@ -103,19 +103,12 @@ namespace :gitlab do Gitlab::Shell.new.remove_all_keys - Gitlab::Shell.new.batch_add_keys do |adder| - Key.find_each(batch_size: 1000) do |key| - adder.add_key(key.shell_id, key.key) - print '.' + Key.find_in_batches(batch_size: 1000) do |keys| + unless Gitlab::Shell.new.batch_add_keys(keys) + puts "Failed to add keys...".color(:red) + exit 1 end end - puts "" - - unless $?.success? - puts "Failed to add keys...".color(:red) - exit 1 - end - rescue Gitlab::TaskAbortedByUserError puts "Quitting...".color(:red) exit 1 diff --git a/lib/tasks/karma.rake b/lib/tasks/karma.rake index 53325d492d1..02987f2beef 100644 --- a/lib/tasks/karma.rake +++ b/lib/tasks/karma.rake @@ -1,13 +1,24 @@ unless Rails.env.production? namespace :karma do desc 'GitLab | Karma | Generate fixtures for JavaScript tests' - RSpec::Core::RakeTask.new(:fixtures, [:pattern]) do |t, args| + task fixtures: ['karma:copy_emojis_from_public_folder', 'karma:rspec_fixtures'] + + desc 'GitLab | Karma | Generate fixtures using RSpec' + RSpec::Core::RakeTask.new(:rspec_fixtures, [:pattern]) do |t, args| args.with_defaults(pattern: '{spec,ee/spec}/javascripts/fixtures/*.rb') ENV['NO_KNAPSACK'] = 'true' t.pattern = args[:pattern] t.rspec_opts = '--format documentation' end + desc 'GitLab | Karma | Copy emojis file' + task :copy_emojis_from_public_folder do + # Copying the emojis.json from the public folder + fixture_file_name = Rails.root.join('spec/javascripts/fixtures/emojis/emojis.json') + FileUtils.mkdir_p(File.dirname(fixture_file_name)) + FileUtils.cp(Rails.root.join('public/-/emojis/1/emojis.json'), fixture_file_name) + end + desc 'GitLab | Karma | Run JavaScript tests' task tests: ['yarn:check'] do sh "yarn run karma" do |ok, res| diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 31b9af92805..53235ee7444 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -19,6 +19,9 @@ msgstr "" msgid " Status" msgstr "" +msgid " You need to do this before %{grace_period_deadline}." +msgstr "" + msgid " or " msgstr "" @@ -1380,6 +1383,9 @@ msgstr "" msgid "Cannot render the image. Maximum character count (%{charLimit}) has been exceeded." msgstr "" +msgid "Cannot skip two factor authentication setup" +msgstr "" + msgid "Certificate" msgstr "" @@ -4328,6 +4334,9 @@ msgstr "" msgid "Invalid input, please avoid emojis" msgstr "" +msgid "Invalid pin code" +msgstr "" + msgid "Invitation" msgstr "" @@ -4872,9 +4881,6 @@ msgstr "" msgid "MergeRequest|No files found" msgstr "" -msgid "MergeRequest|Search files" -msgstr "" - msgid "Merged" msgstr "" @@ -5980,7 +5986,7 @@ msgstr "" msgid "Profiles|This email will be displayed on your public profile" msgstr "" -msgid "Profiles|This email will be used for web based operations, such as edits and merges. %{learn_more}" +msgid "Profiles|This email will be used for web based operations, such as edits and merges. %{commit_email_link_start}Learn more%{commit_email_link_end}" msgstr "" msgid "Profiles|This emoji and message will appear on your profile and throughout the interface." @@ -6722,6 +6728,9 @@ msgstr "" msgid "Scope" msgstr "" +msgid "Scope not supported with disabled 'users_search' feature!" +msgstr "" + msgid "Scroll down to <strong>Google Code Project Hosting</strong> and enable the switch on the right." msgstr "" @@ -7612,6 +7621,12 @@ msgstr "" msgid "The fork relationship has been removed." msgstr "" +msgid "The global settings require you to enable Two-Factor Authentication for your account." +msgstr "" + +msgid "The group settings for %{group_links} require you to enable Two-Factor Authentication for your account. You can %{leave_group_links}." +msgstr "" + msgid "The import will time out after %{timeout}. For repositories that take longer, use a clone/push combination." msgstr "" @@ -9008,6 +9023,12 @@ msgstr "" msgid "Your U2F device needs to be set up. Plug it in (if not already) and click the button on the left." msgstr "" +msgid "Your U2F device was registered!" +msgstr "" + +msgid "Your account uses dedicated credentials for the \"%{group_name}\" group and can only be updated through SSO." +msgstr "" + msgid "Your applications (%{size})" msgstr "" @@ -9126,6 +9147,18 @@ msgstr "" msgid "estimateCommand|%{slash_command} will update the estimated time with the latest command." msgstr "" +msgid "for %{link_to_merge_request} with %{link_to_merge_request_source_branch}" +msgstr "" + +msgid "for %{link_to_merge_request} with %{link_to_merge_request_source_branch} into %{link_to_merge_request_target_branch}" +msgstr "" + +msgid "for %{link_to_pipeline_ref}" +msgstr "" + +msgid "for %{ref}" +msgstr "" + msgid "for this project" msgstr "" @@ -9180,6 +9213,9 @@ msgstr "" msgid "latest version" msgstr "" +msgid "leave %{group_name}" +msgstr "" + msgid "manual" msgstr "" diff --git a/package.json b/package.json index 7d14ccc5a5e..d830d83b963 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "prettier-all": "node ./scripts/frontend/prettier.js check-all", "prettier-all-save": "node ./scripts/frontend/prettier.js save-all", "stylelint": "node node_modules/stylelint/bin/stylelint.js app/assets/stylesheets/**/*.* --custom-formatter node_modules/stylelint-error-string-formatter", + "test": "yarn jest && yarn karma", "webpack": "webpack --config config/webpack.config.js", "webpack-prod": "NODE_ENV=production webpack --config config/webpack.config.js" }, @@ -30,7 +31,7 @@ "@babel/preset-env": "^7.3.1", "@gitlab/csslab": "^1.8.0", "@gitlab/svgs": "^1.54.0", - "@gitlab/ui": "^2.2.3", + "@gitlab/ui": "^2.3.0", "apollo-boost": "^0.3.1", "apollo-client": "^2.5.1", "at.js": "^1.5.4", diff --git a/qa/.gitignore b/qa/.gitignore index 102f7e5e54d..b0ae074ac07 100644 --- a/qa/.gitignore +++ b/qa/.gitignore @@ -1,3 +1,3 @@ tmp/ .ruby-version -urls.txt +urls.yml diff --git a/qa/Rakefile b/qa/Rakefile index b6ad09f9b00..d0101740f1a 100644 --- a/qa/Rakefile +++ b/qa/Rakefile @@ -16,3 +16,25 @@ desc "Generate Performance Testdata" task :generate_perf_testdata do QA::Tools::GeneratePerfTestdata.new.run end + +desc "Run artillery load tests" +task :run_artillery_load_tests do + unless ENV['HOST_URL'] && ENV['LARGE_ISSUE_URL'] && ENV['LARGE_MR_URL'] + urls_file = ENV['URLS_FILE_PATH'] || 'urls.yml' + + unless File.exist?(urls_file) + raise "\n#{urls_file} file is missing. Please provide correct URLS_FILE_PATH or all of HOST_URL, LARGE_ISSUE_URL and LARGE_MR_URL\n\n" + end + + urls = YAML.safe_load(File.read(urls_file)) + ENV['HOST_URL'] = urls[:host] + ENV['LARGE_ISSUE_URL'] = urls[:large_issue] + ENV['LARGE_MR_URL'] = urls[:large_mr] + end + + sh('artillery run load/artillery.yml -o report.json') + sh('artillery report report.json -o report.html && rm report.json') +end + +desc "Generate data and run load tests" +task generate_data_and_run_load_test: [:generate_perf_testdata, :run_artillery_load_tests] diff --git a/qa/load/artillery.yml b/qa/load/artillery.yml new file mode 100644 index 00000000000..e2c3c293d8b --- /dev/null +++ b/qa/load/artillery.yml @@ -0,0 +1,22 @@ +config: + target: "{{ $processEnvironment.HOST_URL }}" + phases: + - duration: 60 + arrivalRate: 1 + name: "Warm up" + - duration: 120 + arrivalRate: 1 + rampTo: 50 + name: "Gradual ramp up" + - duration: 60 + arrivalRate: 50 + name: "Sustained max load" +scenarios: + - name: "Visit large issue url" + flow: + - get: + url: "{{ $processEnvironment.LARGE_ISSUE_URL }}" + - name: "Visit large MR url" + flow: + - get: + url: "{{ $processEnvironment.LARGE_MR_URL }}" diff --git a/qa/qa/page/project/activity.rb b/qa/qa/page/project/activity.rb index 56fbaa90790..afd4f49a844 100644 --- a/qa/qa/page/project/activity.rb +++ b/qa/qa/page/project/activity.rb @@ -6,7 +6,7 @@ module QA element :push_events, "event_filter_link EventFilter::PUSH, _('Push events')" # rubocop:disable QA/ElementWithPattern end - def go_to_push_events + def click_push_events click_on 'Push events' end end diff --git a/qa/qa/page/project/menu.rb b/qa/qa/page/project/menu.rb index 46dfe87fe25..3fe048f752a 100644 --- a/qa/qa/page/project/menu.rb +++ b/qa/qa/page/project/menu.rb @@ -30,7 +30,7 @@ module QA end end - def go_to_activity + def click_activity within_sidebar do click_element(:activity_link) end diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/view_project_activity_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/view_project_activity_spec.rb index d4cedc9362d..fe92fbd3ffe 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/project/view_project_activity_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/project/view_project_activity_spec.rb @@ -14,8 +14,8 @@ module QA end project_push.project.visit! - Page::Project::Menu.perform(&:go_to_activity) - Page::Project::Activity.perform(&:go_to_push_events) + Page::Project::Menu.perform(&:click_activity) + Page::Project::Activity.perform(&:click_push_events) expect(page).to have_content('pushed new branch master') end diff --git a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb index 16aca7c0e22..0b92ea29ca4 100644 --- a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb +++ b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb @@ -116,12 +116,6 @@ module QA p.description = 'Project with AutoDevOps' end - project.visit! - - Page::Alert::AutoDevopsAlert.perform do |alert| - expect(alert).to have_text(/.*The Auto DevOps pipeline has been enabled.*/) - end - # Create AutoDevOps repo Resource::Repository::ProjectPush.fabricate! do |push| push.project = project diff --git a/qa/qa/tools/generate_perf_testdata.rb b/qa/qa/tools/generate_perf_testdata.rb index ad515014794..0a0dbdf5b15 100644 --- a/qa/qa/tools/generate_perf_testdata.rb +++ b/qa/qa/tools/generate_perf_testdata.rb @@ -2,6 +2,7 @@ require 'securerandom' require 'faker' +require 'yaml' require_relative '../../qa' # This script generates testdata for Performance Testing. # Required environment variables: PERSONAL_ACCESS_TOKEN and GITLAB_ADDRESS @@ -20,7 +21,8 @@ module QA @api_client = Runtime::API::Client.new(ENV['GITLAB_ADDRESS'], personal_access_token: ENV['PERSONAL_ACCESS_TOKEN']) @group_name = "gitlab-qa-perf-sandbox-#{SecureRandom.hex(8)}" @project_name = "my-test-project-#{SecureRandom.hex(8)}" - @urls = {} + @visibility = "public" + @urls = { host: ENV['GITLAB_ADDRESS'] } end def run @@ -39,26 +41,26 @@ module QA threads_arr = [] methods_arr.each do |m| - threads_arr << Thread.new {m.call} + threads_arr << Thread.new { m.call } end threads_arr.each(&:join) STDOUT.puts "\nURLs: #{@urls}" - File.open("urls.txt", "w") { |file| file.puts @urls.to_s} + File.open("urls.yml", "w") { |file| file.puts @urls.to_yaml } STDOUT.puts "\nDone" end private def create_group - group_search_response = post Runtime::API::Request.new(@api_client, "/groups").url, "name=#{@group_name}&path=#{@group_name}" + group_search_response = post Runtime::API::Request.new(@api_client, "/groups").url, "name=#{@group_name}&path=#{@group_name}&visibility=#{@visibility}" group = JSON.parse(group_search_response.body) @urls[:group_page] = group["web_url"] group["id"] end def create_project(group_id) - create_project_response = post Runtime::API::Request.new(@api_client, "/projects").url, "name=#{@project_name}&namespace_id=#{group_id}" + create_project_response = post Runtime::API::Request.new(@api_client, "/projects").url, "name=#{@project_name}&namespace_id=#{group_id}&visibility=#{@visibility}" @urls[:project_page] = JSON.parse(create_project_response.body)["web_url"] end diff --git a/scripts/build_assets_image b/scripts/build_assets_image index b659bd751b2..25b6060b6c4 100755 --- a/scripts/build_assets_image +++ b/scripts/build_assets_image @@ -7,14 +7,11 @@ then fi # Generate the image name based on the project this is being run in -ASSETS_IMAGE_NAME=$(echo ${CI_PROJECT_NAME} | - awk '{ - split($1, p, "-"); - interim = sprintf("%s-assets-%s", p[1], p[2]); - sub(/-$/, "", interim); - print interim - }' -) +ASSETS_IMAGE_NAME="gitlab-assets-ce" +if [[ "${CI_PROJECT_NAME}" == "gitlab-ee" ]] +then + ASSETS_IMAGE_NAME="gitlab-assets-ee" +fi ASSETS_IMAGE_PATH=${CI_REGISTRY}/${CI_PROJECT_PATH}/${ASSETS_IMAGE_NAME} diff --git a/scripts/review_apps/review-apps.sh b/scripts/review_apps/review-apps.sh index f610485a700..32fce946c17 100755 --- a/scripts/review_apps/review-apps.sh +++ b/scripts/review_apps/review-apps.sh @@ -4,6 +4,16 @@ export TILLER_NAMESPACE="$KUBE_NAMESPACE" function echoerr() { printf "\033[0;31m%s\n\033[0m" "$*" >&2; } function echoinfo() { printf "\033[0;33m%s\n\033[0m" "$*" >&2; } +function perform_review_app_deployment() { + check_kube_domain + download_gitlab_chart + ensure_namespace + install_tiller + install_external_dns + time deploy + add_license +} + function check_kube_domain() { if [ -z ${REVIEW_APPS_DOMAIN+x} ]; then echo "In order to deploy or use Review Apps, REVIEW_APPS_DOMAIN variable must be set" diff --git a/spec/controllers/admin/projects_controller_spec.rb b/spec/controllers/admin/projects_controller_spec.rb index 8166657f674..4caf8b46519 100644 --- a/spec/controllers/admin/projects_controller_spec.rb +++ b/spec/controllers/admin/projects_controller_spec.rb @@ -43,6 +43,16 @@ describe Admin::ProjectsController do end end + describe 'GET /projects.json' do + render_views + + before do + get :index, format: :json + end + + it { is_expected.to respond_with(:success) } + end + describe 'GET /projects/:id' do render_views diff --git a/spec/controllers/dashboard/projects_controller_spec.rb b/spec/controllers/dashboard/projects_controller_spec.rb index 2975205e09c..649441f4917 100644 --- a/spec/controllers/dashboard/projects_controller_spec.rb +++ b/spec/controllers/dashboard/projects_controller_spec.rb @@ -2,4 +2,30 @@ require 'spec_helper' describe Dashboard::ProjectsController do it_behaves_like 'authenticates sessionless user', :index, :atom + + context 'json requests' do + render_views + + let(:user) { create(:user) } + + before do + sign_in(user) + end + + describe 'GET /projects.json' do + before do + get :index, format: :json + end + + it { is_expected.to respond_with(:success) } + end + + describe 'GET /starred.json' do + before do + get :starred, format: :json + end + + it { is_expected.to respond_with(:success) } + end + end end diff --git a/spec/controllers/explore/projects_controller_spec.rb b/spec/controllers/explore/projects_controller_spec.rb index d57367e931e..7e20ddca249 100644 --- a/spec/controllers/explore/projects_controller_spec.rb +++ b/spec/controllers/explore/projects_controller_spec.rb @@ -1,6 +1,36 @@ require 'spec_helper' describe Explore::ProjectsController do + describe 'GET #index.json' do + render_views + + before do + get :index, format: :json + end + + it { is_expected.to respond_with(:success) } + end + + describe 'GET #trending.json' do + render_views + + before do + get :trending, format: :json + end + + it { is_expected.to respond_with(:success) } + end + + describe 'GET #starred.json' do + render_views + + before do + get :starred, format: :json + end + + it { is_expected.to respond_with(:success) } + end + describe 'GET #trending' do context 'sorting by update date' do let(:project1) { create(:project, :public, updated_at: 3.days.ago) } diff --git a/spec/controllers/omniauth_callbacks_controller_spec.rb b/spec/controllers/omniauth_callbacks_controller_spec.rb index e0da23ca0b8..06c6f49f7cc 100644 --- a/spec/controllers/omniauth_callbacks_controller_spec.rb +++ b/spec/controllers/omniauth_callbacks_controller_spec.rb @@ -113,6 +113,33 @@ describe OmniauthCallbacksController, type: :controller do expect(request.env['warden']).to be_authenticated end + context 'when user has no linked provider' do + let(:user) { create(:user) } + + before do + sign_in user + end + + it 'links identity' do + expect do + post provider + user.reload + end.to change { user.identities.count }.by(1) + end + + context 'and is not allowed to link the provider' do + before do + allow_any_instance_of(IdentityProviderPolicy).to receive(:can?).with(:link).and_return(false) + end + + it 'returns 403' do + post provider + + expect(response).to have_gitlab_http_status(403) + end + end + end + shared_context 'sign_up' do let(:user) { double(email: 'new@example.com') } diff --git a/spec/controllers/projects/git_http_controller_spec.rb b/spec/controllers/projects/git_http_controller_spec.rb new file mode 100644 index 00000000000..bf099e8deeb --- /dev/null +++ b/spec/controllers/projects/git_http_controller_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Projects::GitHttpController do + describe 'HEAD #info_refs' do + it 'returns 403' do + project = create(:project, :public, :repository) + + head :info_refs, params: { namespace_id: project.namespace.to_param, project_id: project.path + '.git' } + + expect(response.status).to eq(403) + end + end +end diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb index 0b0f5117784..deecb7fefe9 100644 --- a/spec/controllers/projects/notes_controller_spec.rb +++ b/spec/controllers/projects/notes_controller_spec.rb @@ -413,6 +413,37 @@ describe Projects::NotesController do end end end + + context 'when creating a note with quick actions' do + context 'with commands that return changes' do + let(:note_text) { "/award :thumbsup:\n/estimate 1d\n/spend 3h" } + + it 'includes changes in commands_changes ' do + post :create, params: request_params.merge(note: { note: note_text }, format: :json) + + expect(response).to have_gitlab_http_status(200) + expect(json_response['commands_changes']).to include('emoji_award', 'time_estimate', 'spend_time') + expect(json_response['commands_changes']).not_to include('target_project', 'title') + end + end + + context 'with commands that do not return changes' do + let(:issue) { create(:issue, project: project) } + let(:other_project) { create(:project) } + let(:note_text) { "/move #{other_project.full_path}\n/title AAA" } + + before do + other_project.add_developer(user) + end + + it 'does not include changes in commands_changes' do + post :create, params: request_params.merge(note: { note: note_text }, target_type: 'issue', target_id: issue.id, format: :json) + + expect(response).to have_gitlab_http_status(200) + expect(json_response['commands_changes']).not_to include('target_project', 'title') + end + end + end end describe 'PUT update' do diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb index fd151e8a298..c1baf88778d 100644 --- a/spec/controllers/registrations_controller_spec.rb +++ b/spec/controllers/registrations_controller_spec.rb @@ -15,7 +15,7 @@ describe RegistrationsController do context 'when send_user_confirmation_email is false' do it 'signs the user in' do - allow_any_instance_of(ApplicationSetting).to receive(:send_user_confirmation_email).and_return(false) + stub_application_setting(send_user_confirmation_email: false) expect { post(:create, params: user_params) }.not_to change { ActionMailer::Base.deliveries.size } expect(subject.current_user).not_to be_nil @@ -24,7 +24,7 @@ describe RegistrationsController do context 'when send_user_confirmation_email is true' do it 'does not authenticate user and sends confirmation email' do - allow_any_instance_of(ApplicationSetting).to receive(:send_user_confirmation_email).and_return(true) + stub_application_setting(send_user_confirmation_email: true) post(:create, params: user_params) @@ -35,7 +35,7 @@ describe RegistrationsController do context 'when signup_enabled? is false' do it 'redirects to sign_in' do - allow_any_instance_of(ApplicationSetting).to receive(:signup_enabled?).and_return(false) + stub_application_setting(signup_enabled: false) expect { post(:create, params: user_params) }.not_to change(User, :count) expect(response).to redirect_to(new_user_session_path) diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb index 986f3823275..8eb413bdd8d 100644 --- a/spec/features/issues/gfm_autocomplete_spec.rb +++ b/spec/features/issues/gfm_autocomplete_spec.rb @@ -278,12 +278,7 @@ describe 'GFM autocomplete', :js do end end - # This context has just one example in each contexts in order to improve spec performance. - context 'labels', :quarantine do - let!(:backend) { create(:label, project: project, title: 'backend') } - let!(:bug) { create(:label, project: project, title: 'bug') } - let!(:feature_proposal) { create(:label, project: project, title: 'feature proposal') } - + context 'labels' do it 'opens autocomplete menu for Labels when field starts with text with item escaping HTML characters' do create(:label, project: project, title: label_xss_title) @@ -298,83 +293,6 @@ describe 'GFM autocomplete', :js do expect(find('.atwho-view-ul').text).to have_content('alert label') end end - - context 'when no labels are assigned' do - it 'shows labels' do - note = find('#note-body') - - # It should show all the labels on "~". - type(note, '~') - wait_for_requests - expect_labels(shown: [backend, bug, feature_proposal]) - - # It should show all the labels on "/label ~". - type(note, '/label ~') - expect_labels(shown: [backend, bug, feature_proposal]) - - # It should show all the labels on "/relabel ~". - type(note, '/relabel ~') - expect_labels(shown: [backend, bug, feature_proposal]) - - # It should show no labels on "/unlabel ~". - type(note, '/unlabel ~') - expect_labels(not_shown: [backend, bug, feature_proposal]) - end - end - - context 'when some labels are assigned' do - before do - issue.labels << [backend] - end - - it 'shows labels' do - note = find('#note-body') - - # It should show all the labels on "~". - type(note, '~') - wait_for_requests - expect_labels(shown: [backend, bug, feature_proposal]) - - # It should show only unset labels on "/label ~". - type(note, '/label ~') - expect_labels(shown: [bug, feature_proposal], not_shown: [backend]) - - # It should show all the labels on "/relabel ~". - type(note, '/relabel ~') - expect_labels(shown: [backend, bug, feature_proposal]) - - # It should show only set labels on "/unlabel ~". - type(note, '/unlabel ~') - expect_labels(shown: [backend], not_shown: [bug, feature_proposal]) - end - end - - context 'when all labels are assigned' do - before do - issue.labels << [backend, bug, feature_proposal] - end - - it 'shows labels' do - note = find('#note-body') - - # It should show all the labels on "~". - type(note, '~') - wait_for_requests - expect_labels(shown: [backend, bug, feature_proposal]) - - # It should show no labels on "/label ~". - type(note, '/label ~') - expect_labels(not_shown: [backend, bug, feature_proposal]) - - # It should show all the labels on "/relabel ~". - type(note, '/relabel ~') - expect_labels(shown: [backend, bug, feature_proposal]) - - # It should show all the labels on "/unlabel ~". - type(note, '/unlabel ~') - expect_labels(shown: [backend, bug, feature_proposal]) - end - end end shared_examples 'autocomplete suggestions' do diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index 9bc340ed4bb..51508b78649 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -497,12 +497,21 @@ describe 'Issues' do it 'allows user to unselect themselves', :js do issue2 = create(:issue, project: project, author: user) + visit project_issue_path(project, issue2) + def close_dropdown_menu_if_visible + find('.dropdown-menu-toggle', visible: :all).tap do |toggle| + toggle.click if toggle.visible? + end + end + page.within '.assignee' do click_link 'Edit' click_link user.name + close_dropdown_menu_if_visible + page.within '.value .author' do expect(page).to have_content user.name end @@ -510,6 +519,8 @@ describe 'Issues' do click_link 'Edit' click_link user.name + close_dropdown_menu_if_visible + page.within '.value .assign-yourself' do expect(page).to have_content "No assignee" end diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb index 36b8c15b8b6..5d2a99f40b7 100644 --- a/spec/features/projects/pipelines/pipeline_spec.rb +++ b/spec/features/projects/pipelines/pipeline_spec.rb @@ -1,6 +1,9 @@ require 'spec_helper' describe 'Pipeline', :js do + include RoutesHelpers + include ProjectForksHelper + let(:project) { create(:project) } let(:user) { create(:user) } let(:role) { :developer } @@ -72,6 +75,15 @@ describe 'Pipeline', :js do expect(page).to have_link(pipeline.ref) end + it 'shows the pipeline information' do + within '.pipeline-info' do + expect(page).to have_content("#{pipeline.statuses.count} jobs " \ + "for #{pipeline.ref} ") + expect(page).to have_link(pipeline.ref, + href: project_commits_path(pipeline.project, pipeline.ref)) + end + end + it_behaves_like 'showing user status' do let(:user_with_status) { pipeline.user } @@ -254,6 +266,113 @@ describe 'Pipeline', :js do expect(page).to have_content(pipeline.ref) end end + + context 'when pipeline is detached merge request pipeline' do + let(:source_project) { project } + let(:target_project) { project } + + let(:merge_request) do + create(:merge_request, + :with_detached_merge_request_pipeline, + source_project: source_project, + target_project: target_project) + end + + let(:pipeline) do + merge_request.all_pipelines.last + end + + it 'shows the pipeline information' do + within '.pipeline-info' do + expect(page).to have_content("#{pipeline.statuses.count} jobs " \ + "for !#{merge_request.iid} " \ + "with #{merge_request.source_branch}") + expect(page).to have_link("!#{merge_request.iid}", + href: project_merge_request_path(project, merge_request)) + expect(page).to have_link(merge_request.source_branch, + href: project_commits_path(merge_request.source_project, merge_request.source_branch)) + end + end + + context 'when source project is a forked project' do + let(:source_project) { fork_project(project, user, repository: true) } + + before do + visit project_pipeline_path(source_project, pipeline) + end + + it 'shows the pipeline information' do + within '.pipeline-info' do + expect(page).to have_content("#{pipeline.statuses.count} jobs " \ + "for !#{merge_request.iid} " \ + "with #{merge_request.source_branch}") + expect(page).to have_link("!#{merge_request.iid}", + href: project_merge_request_path(project, merge_request)) + expect(page).to have_link(merge_request.source_branch, + href: project_commits_path(merge_request.source_project, merge_request.source_branch)) + end + end + end + end + + context 'when pipeline is merge request pipeline' do + let(:source_project) { project } + let(:target_project) { project } + + let(:merge_request) do + create(:merge_request, + :with_merge_request_pipeline, + source_project: source_project, + target_project: target_project, + merge_sha: project.commit.id) + end + + let(:pipeline) do + merge_request.all_pipelines.last + end + + before do + pipeline.update(user: user) + end + + it 'shows the pipeline information' do + within '.pipeline-info' do + expect(page).to have_content("#{pipeline.statuses.count} jobs " \ + "for !#{merge_request.iid} " \ + "with #{merge_request.source_branch} " \ + "into #{merge_request.target_branch}") + expect(page).to have_link("!#{merge_request.iid}", + href: project_merge_request_path(project, merge_request)) + expect(page).to have_link(merge_request.source_branch, + href: project_commits_path(merge_request.source_project, merge_request.source_branch)) + expect(page).to have_link(merge_request.target_branch, + href: project_commits_path(merge_request.target_project, merge_request.target_branch)) + end + end + + context 'when source project is a forked project' do + let(:source_project) { fork_project(project, user, repository: true) } + + before do + visit project_pipeline_path(source_project, pipeline) + end + + it 'shows the pipeline information' do + within '.pipeline-info' do + expect(page).to have_content("#{pipeline.statuses.count} jobs " \ + "for !#{merge_request.iid} " \ + "with #{merge_request.source_branch} " \ + "into #{merge_request.target_branch}") + expect(page).to have_link("!#{merge_request.iid}", + href: project_merge_request_path(project, merge_request)) + expect(page).to have_link(merge_request.source_branch, + href: project_commits_path(merge_request.source_project, merge_request.source_branch)) + expect(page).to have_link(merge_request.target_branch, + href: project_commits_path(merge_request.target_project, merge_request.target_branch)) + end + end + end + end end context 'when user does not have access to read jobs' do @@ -686,9 +805,9 @@ describe 'Pipeline', :js do visit project_pipeline_path(project, pipeline) end - it 'contains badge that indicates merge request pipeline' do + it 'contains badge that indicates detached merge request pipeline' do page.within(all('.well-segment')[1]) do - expect(page).to have_content 'merge request' + expect(page).to have_content 'detached' end end end diff --git a/spec/features/search/user_searches_for_users_spec.rb b/spec/features/search/user_searches_for_users_spec.rb new file mode 100644 index 00000000000..3725143291d --- /dev/null +++ b/spec/features/search/user_searches_for_users_spec.rb @@ -0,0 +1,83 @@ +require 'spec_helper' + +describe 'User searches for users' do + context 'when on the dashboard' do + it 'finds the user' do + create(:user, username: 'gob_bluth', name: 'Gob Bluth') + + sign_in(create(:user)) + + visit dashboard_projects_path + + fill_in 'search', with: 'gob' + click_button 'Go' + + expect(page).to have_content('Users 1') + + click_on('Users 1') + + expect(page).to have_content('Gob Bluth') + expect(page).to have_content('@gob_bluth') + end + end + + context 'when on the project page' do + it 'finds the user belonging to the project' do + project = create(:project) + + user1 = create(:user, username: 'gob_bluth', name: 'Gob Bluth') + create(:project_member, :developer, user: user1, project: project) + + user2 = create(:user, username: 'michael_bluth', name: 'Michael Bluth') + create(:project_member, :developer, user: user2, project: project) + + create(:user, username: 'gob_2018', name: 'George Oscar Bluth') + + sign_in(user1) + + visit projects_path(project) + + fill_in 'search', with: 'gob' + click_button 'Go' + + expect(page).to have_content('Gob Bluth') + expect(page).to have_content('@gob_bluth') + + expect(page).not_to have_content('Michael Bluth') + expect(page).not_to have_content('@michael_bluth') + + expect(page).not_to have_content('George Oscar Bluth') + expect(page).not_to have_content('@gob_2018') + end + end + + context 'when on the group page' do + it 'finds the user belonging to the group' do + group = create(:group) + + user1 = create(:user, username: 'gob_bluth', name: 'Gob Bluth') + create(:group_member, :developer, user: user1, group: group) + + user2 = create(:user, username: 'michael_bluth', name: 'Michael Bluth') + create(:group_member, :developer, user: user2, group: group) + + create(:user, username: 'gob_2018', name: 'George Oscar Bluth') + + sign_in(user1) + + visit group_path(group) + + fill_in 'search', with: 'gob' + click_button 'Go' + + expect(page).to have_content('Gob Bluth') + expect(page).to have_content('@gob_bluth') + + expect(page).not_to have_content('Michael Bluth') + expect(page).not_to have_content('@michael_bluth') + + expect(page).not_to have_content('George Oscar Bluth') + expect(page).not_to have_content('@gob_2018') + end + end +end diff --git a/spec/features/user_opens_link_to_comment.rb b/spec/features/user_opens_link_to_comment.rb new file mode 100644 index 00000000000..f1e07e55799 --- /dev/null +++ b/spec/features/user_opens_link_to_comment.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'User opens link to comment', :js do + let(:project) { create(:project, :public) } + let(:note) { create(:note_on_issue, project: project) } + + context 'authenticated user' do + let(:user) { create(:user) } + + before do + sign_in(user) + end + + it 'switches to all activity and does not show error message' do + create(:user_preference, user: user, issue_notes_filter: UserPreference::NOTES_FILTERS[:only_activity]) + + visit Gitlab::UrlBuilder.build(note) + + expect(page.find('#discussion-filter-dropdown')).to have_content('Show all activity') + expect(page).not_to have_content('Something went wrong while fetching comments') + end + end + + context 'anonymous user' do + it 'does not show error message' do + visit Gitlab::UrlBuilder.build(note) + + expect(page).not_to have_content('Something went wrong while fetching comments') + end + end +end diff --git a/spec/features/users/login_spec.rb b/spec/features/users/login_spec.rb index ad856bd062e..368a814874f 100644 --- a/spec/features/users/login_spec.rb +++ b/spec/features/users/login_spec.rb @@ -434,16 +434,22 @@ describe 'Login' do context 'within the grace period' do it 'redirects to two-factor configuration page' do - expect(authentication_metrics) - .to increment(:user_authenticated_counter) - - gitlab_sign_in(user) - - expect(current_path).to eq profile_two_factor_auth_path - expect(page).to have_content( - 'The group settings for Group 1 and Group 2 require you to enable ' \ - 'Two-Factor Authentication for your account. You need to do this ' \ - 'before ') + Timecop.freeze do + expect(authentication_metrics) + .to increment(:user_authenticated_counter) + + gitlab_sign_in(user) + + expect(current_path).to eq profile_two_factor_auth_path + expect(page).to have_content( + 'The group settings for Group 1 and Group 2 require you to enable '\ + 'Two-Factor Authentication for your account. '\ + 'You can leave Group 1 and leave Group 2. '\ + 'You need to do this '\ + 'before '\ + "#{(Time.zone.now + 2.days).strftime("%a, %-d %b %Y %H:%M:%S %z")}" + ) + end end it 'allows skipping two-factor configuration', :js do @@ -500,7 +506,8 @@ describe 'Login' do expect(current_path).to eq profile_two_factor_auth_path expect(page).to have_content( 'The group settings for Group 1 and Group 2 require you to enable ' \ - 'Two-Factor Authentication for your account.' + 'Two-Factor Authentication for your account. '\ + 'You can leave Group 1 and leave Group 2.' ) end end diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb index 55efab7dec3..f74eb1364a6 100644 --- a/spec/finders/issues_finder_spec.rb +++ b/spec/finders/issues_finder_spec.rb @@ -220,6 +220,7 @@ describe IssuesFinder do let(:yesterday) { Date.today - 1.day } let(:tomorrow) { Date.today + 1.day } let(:two_days_ago) { Date.today - 2.days } + let(:three_days_ago) { Date.today - 3.days } let(:milestones) do [ @@ -227,6 +228,8 @@ describe IssuesFinder do create(:milestone, project: project_started_1_and_2, title: '1.0', start_date: two_days_ago), create(:milestone, project: project_started_1_and_2, title: '2.0', start_date: yesterday), create(:milestone, project: project_started_1_and_2, title: '3.0', start_date: tomorrow), + create(:milestone, :closed, project: project_started_1_and_2, title: '4.0', start_date: three_days_ago), + create(:milestone, :closed, project: project_started_8, title: '6.0', start_date: three_days_ago), create(:milestone, project: project_started_8, title: '7.0'), create(:milestone, project: project_started_8, title: '8.0', start_date: yesterday), create(:milestone, project: project_started_8, title: '9.0', start_date: tomorrow) diff --git a/spec/frontend/gfm_auto_complete_spec.js b/spec/frontend/gfm_auto_complete_spec.js index 27c679c749b..ed12af925f1 100644 --- a/spec/frontend/gfm_auto_complete_spec.js +++ b/spec/frontend/gfm_auto_complete_spec.js @@ -6,17 +6,26 @@ import GfmAutoComplete from '~/gfm_auto_complete'; import 'jquery.caret'; import 'at.js'; +import { TEST_HOST } from 'helpers/test_constants'; +import { setTestTimeout } from 'helpers/timeout'; +import { getJSONFixture } from 'helpers/fixtures'; + +setTestTimeout(500); + +const labelsFixture = getJSONFixture('autocomplete_sources/labels.json'); + describe('GfmAutoComplete', () => { const gfmAutoCompleteCallbacks = GfmAutoComplete.prototype.getDefaultCallbacks.call({ fetchData: () => {}, }); let atwhoInstance; - let items; let sorterValue; describe('DefaultOptions.sorter', () => { describe('assets loading', () => { + let items; + beforeEach(() => { jest.spyOn(GfmAutoComplete, 'isLoading').mockReturnValue(true); @@ -61,7 +70,7 @@ describe('GfmAutoComplete', () => { atwhoInstance = { setting: {} }; const query = 'query'; - items = []; + const items = []; const searchKey = 'searchKey'; gfmAutoCompleteCallbacks.sorter.call(atwhoInstance, query, items, searchKey); @@ -250,4 +259,90 @@ describe('GfmAutoComplete', () => { ).toBe('<li><small>grp/proj#5</small> Some Issue</li>'); }); }); + + describe('labels', () => { + const dataSources = { + labels: `${TEST_HOST}/autocomplete_sources/labels`, + }; + + const allLabels = labelsFixture; + const assignedLabels = allLabels.filter(label => label.set); + const unassignedLabels = allLabels.filter(label => !label.set); + + let autocomplete; + let $textarea; + + beforeEach(() => { + autocomplete = new GfmAutoComplete(dataSources); + $textarea = $('<textarea></textarea>'); + autocomplete.setup($textarea, { labels: true }); + }); + + afterEach(() => { + autocomplete.destroy(); + }); + + const triggerDropdown = text => { + $textarea + .trigger('focus') + .val(text) + .caret('pos', -1); + $textarea.trigger('keyup'); + + return new Promise(window.requestAnimationFrame); + }; + + const getDropdownItems = () => { + const dropdown = document.getElementById('at-view-labels'); + const items = dropdown.getElementsByTagName('li'); + return [].map.call(items, item => item.textContent.trim()); + }; + + const expectLabels = ({ input, output }) => + triggerDropdown(input).then(() => { + expect(getDropdownItems()).toEqual(output.map(label => label.title)); + }); + + describe('with no labels assigned', () => { + beforeEach(() => { + autocomplete.cachedData['~'] = [...unassignedLabels]; + }); + + it.each` + input | output + ${'~'} | ${unassignedLabels} + ${'/label ~'} | ${unassignedLabels} + ${'/relabel ~'} | ${unassignedLabels} + ${'/unlabel ~'} | ${[]} + `('$input shows $output.length labels', expectLabels); + }); + + describe('with some labels assigned', () => { + beforeEach(() => { + autocomplete.cachedData['~'] = allLabels; + }); + + it.each` + input | output + ${'~'} | ${allLabels} + ${'/label ~'} | ${unassignedLabels} + ${'/relabel ~'} | ${allLabels} + ${'/unlabel ~'} | ${assignedLabels} + `('$input shows $output.length labels', expectLabels); + }); + + describe('with all labels assigned', () => { + beforeEach(() => { + autocomplete.cachedData['~'] = [...assignedLabels]; + }); + + it.each` + input | output + ${'~'} | ${assignedLabels} + ${'/label ~'} | ${[]} + ${'/relabel ~'} | ${assignedLabels} + ${'/unlabel ~'} | ${assignedLabels} + `('$input shows $output.length labels', expectLabels); + }); + }); }); diff --git a/spec/frontend/helpers/fixtures.js b/spec/frontend/helpers/fixtures.js new file mode 100644 index 00000000000..f96f27c4d80 --- /dev/null +++ b/spec/frontend/helpers/fixtures.js @@ -0,0 +1,24 @@ +/* eslint-disable import/prefer-default-export, global-require, import/no-dynamic-require */ + +import fs from 'fs'; +import path from 'path'; + +// jest-util is part of Jest +// eslint-disable-next-line import/no-extraneous-dependencies +import { ErrorWithStack } from 'jest-util'; + +const fixturesBasePath = path.join(process.cwd(), 'spec', 'javascripts', 'fixtures'); + +export function getJSONFixture(relativePath, ee = false) { + const absolutePath = path.join(fixturesBasePath, ee ? 'ee' : '', relativePath); + if (!fs.existsSync(absolutePath)) { + throw new ErrorWithStack( + `Fixture file ${relativePath} does not exist. + +Did you run bin/rake karma:fixtures?`, + getJSONFixture, + ); + } + + return require(absolutePath); +} diff --git a/spec/helpers/auth_helper_spec.rb b/spec/helpers/auth_helper_spec.rb index f0c2e4768ec..2ba8b3dbf22 100644 --- a/spec/helpers/auth_helper_spec.rb +++ b/spec/helpers/auth_helper_spec.rb @@ -97,17 +97,37 @@ describe AuthHelper do end end - describe 'unlink_allowed?' do - [:saml, :cas3].each do |provider| - it "returns true if the provider is #{provider}" do - expect(helper.unlink_allowed?(provider)).to be false - end + describe '#link_provider_allowed?' do + let(:policy) { instance_double('IdentityProviderPolicy') } + let(:current_user) { instance_double('User') } + let(:provider) { double } + + before do + allow(helper).to receive(:current_user).and_return(current_user) + allow(IdentityProviderPolicy).to receive(:new).with(current_user, provider).and_return(policy) end - [:twitter, :facebook, :google_oauth2, :gitlab, :github, :bitbucket, :crowd, :auth0, :authentiq].each do |provider| - it "returns false if the provider is #{provider}" do - expect(helper.unlink_allowed?(provider)).to be true - end + it 'delegates to identity provider policy' do + allow(policy).to receive(:can?).with(:link).and_return('policy_link_result') + + expect(helper.link_provider_allowed?(provider)).to eq 'policy_link_result' + end + end + + describe '#unlink_provider_allowed?' do + let(:policy) { instance_double('IdentityProviderPolicy') } + let(:current_user) { instance_double('User') } + let(:provider) { double } + + before do + allow(helper).to receive(:current_user).and_return(current_user) + allow(IdentityProviderPolicy).to receive(:new).with(current_user, provider).and_return(policy) + end + + it 'delegates to identity provider policy' do + allow(policy).to receive(:can?).with(:unlink).and_return('policy_unlink_result') + + expect(helper.unlink_provider_allowed?(provider)).to eq 'policy_unlink_result' end end end diff --git a/spec/javascripts/badges/components/badge_list_spec.js b/spec/javascripts/badges/components/badge_list_spec.js index 536671db377..2f72c9ed89d 100644 --- a/spec/javascripts/badges/components/badge_list_spec.js +++ b/spec/javascripts/badges/components/badge_list_spec.js @@ -60,7 +60,7 @@ describe('BadgeList component', () => { Vue.nextTick() .then(() => { - const loadingIcon = vm.$el.querySelector('.fa-spinner'); + const loadingIcon = vm.$el.querySelector('.spinner'); expect(loadingIcon).toBeVisible(); }) diff --git a/spec/javascripts/badges/components/badge_spec.js b/spec/javascripts/badges/components/badge_spec.js index 29805408bcf..4e4d1ae2e99 100644 --- a/spec/javascripts/badges/components/badge_spec.js +++ b/spec/javascripts/badges/components/badge_spec.js @@ -15,7 +15,7 @@ describe('Badge component', () => { const buttons = vm.$el.querySelectorAll('button'); return { badgeImage: vm.$el.querySelector('img.project-badge'), - loadingIcon: vm.$el.querySelector('.fa-spinner'), + loadingIcon: vm.$el.querySelector('.spinner'), reloadButton: buttons[buttons.length - 1], }; }; diff --git a/spec/javascripts/boards/board_list_spec.js b/spec/javascripts/boards/board_list_spec.js index 2642c8b1bdb..396fc823ef5 100644 --- a/spec/javascripts/boards/board_list_spec.js +++ b/spec/javascripts/boards/board_list_spec.js @@ -195,7 +195,7 @@ describe('Board list component', () => { component.list.loadingMore = true; Vue.nextTick(() => { - expect(component.$el.querySelector('.board-list-count .fa-spinner')).not.toBeNull(); + expect(component.$el.querySelector('.board-list-count .spinner')).not.toBeNull(); done(); }); diff --git a/spec/javascripts/diffs/components/app_spec.js b/spec/javascripts/diffs/components/app_spec.js index d81c433cca6..8d7c52a2876 100644 --- a/spec/javascripts/diffs/components/app_spec.js +++ b/spec/javascripts/diffs/components/app_spec.js @@ -397,4 +397,61 @@ describe('diffs/components/app', () => { expect(wrapper.find(TreeList).exists()).toBe(true); }); }); + + describe('hideTreeListIfJustOneFile', () => { + let toggleShowTreeList; + + beforeEach(() => { + toggleShowTreeList = jasmine.createSpy('toggleShowTreeList'); + }); + + afterEach(() => { + localStorage.removeItem('mr_tree_show'); + }); + + it('calls toggleShowTreeList when only 1 file', () => { + createComponent({}, ({ state }) => { + state.diffs.diffFiles.push({ sha: '123' }); + }); + + wrapper.setMethods({ + toggleShowTreeList, + }); + + wrapper.vm.hideTreeListIfJustOneFile(); + + expect(toggleShowTreeList).toHaveBeenCalledWith(false); + }); + + it('does not call toggleShowTreeList when more than 1 file', () => { + createComponent({}, ({ state }) => { + state.diffs.diffFiles.push({ sha: '123' }); + state.diffs.diffFiles.push({ sha: '124' }); + }); + + wrapper.setMethods({ + toggleShowTreeList, + }); + + wrapper.vm.hideTreeListIfJustOneFile(); + + expect(toggleShowTreeList).not.toHaveBeenCalled(); + }); + + it('does not call toggleShowTreeList when localStorage is set', () => { + localStorage.setItem('mr_tree_show', 'true'); + + createComponent({}, ({ state }) => { + state.diffs.diffFiles.push({ sha: '123' }); + }); + + wrapper.setMethods({ + toggleShowTreeList, + }); + + wrapper.vm.hideTreeListIfJustOneFile(); + + expect(toggleShowTreeList).not.toHaveBeenCalled(); + }); + }); }); diff --git a/spec/javascripts/diffs/store/actions_spec.js b/spec/javascripts/diffs/store/actions_spec.js index 6c637097893..bca99caa920 100644 --- a/spec/javascripts/diffs/store/actions_spec.js +++ b/spec/javascripts/diffs/store/actions_spec.js @@ -734,6 +734,14 @@ describe('DiffsStoreActions', () => { expect(localStorage.setItem).toHaveBeenCalledWith('mr_tree_show', true); }); + + it('does not update localStorage', () => { + spyOn(localStorage, 'setItem'); + + toggleShowTreeList({ commit() {}, state: { showTreeList: true } }, false); + + expect(localStorage.setItem).not.toHaveBeenCalled(); + }); }); describe('renderFileForDiscussionId', () => { diff --git a/spec/javascripts/filtered_search/visual_token_value_spec.js b/spec/javascripts/filtered_search/visual_token_value_spec.js index f52dc26a7bb..14217d460cc 100644 --- a/spec/javascripts/filtered_search/visual_token_value_spec.js +++ b/spec/javascripts/filtered_search/visual_token_value_spec.js @@ -317,7 +317,18 @@ describe('Filtered Search Visual Tokens', () => { it('does not update user token appearance for `none` filter', () => { const { subject, tokenValueContainer, tokenValueElement } = findElements(authorToken); - subject.tokenType = 'none'; + subject.tokenValue = 'none'; + + const { updateUserTokenAppearanceSpy } = setupSpies(subject); + subject.render(tokenValueContainer, tokenValueElement); + + expect(updateUserTokenAppearanceSpy.calls.count()).toBe(0); + }); + + it('does not update user token appearance for `None` filter', () => { + const { subject, tokenValueContainer, tokenValueElement } = findElements(authorToken); + + subject.tokenValue = 'None'; const { updateUserTokenAppearanceSpy } = setupSpies(subject); subject.render(tokenValueContainer, tokenValueElement); @@ -328,7 +339,7 @@ describe('Filtered Search Visual Tokens', () => { it('does not update user token appearance for `any` filter', () => { const { subject, tokenValueContainer, tokenValueElement } = findElements(authorToken); - subject.tokenType = 'any'; + subject.tokenValue = 'any'; const { updateUserTokenAppearanceSpy } = setupSpies(subject); subject.render(tokenValueContainer, tokenValueElement); @@ -336,10 +347,21 @@ describe('Filtered Search Visual Tokens', () => { expect(updateUserTokenAppearanceSpy.calls.count()).toBe(0); }); + it('does not update label token color for `None` filter', () => { + const { subject, tokenValueContainer, tokenValueElement } = findElements(bugLabelToken); + + subject.tokenValue = 'None'; + + const { updateLabelTokenColorSpy } = setupSpies(subject); + subject.render(tokenValueContainer, tokenValueElement); + + expect(updateLabelTokenColorSpy.calls.count()).toBe(0); + }); + it('does not update label token color for `none` filter', () => { const { subject, tokenValueContainer, tokenValueElement } = findElements(bugLabelToken); - subject.tokenType = 'none'; + subject.tokenValue = 'none'; const { updateLabelTokenColorSpy } = setupSpies(subject); subject.render(tokenValueContainer, tokenValueElement); @@ -350,7 +372,7 @@ describe('Filtered Search Visual Tokens', () => { it('does not update label token color for `any` filter', () => { const { subject, tokenValueContainer, tokenValueElement } = findElements(bugLabelToken); - subject.tokenType = 'any'; + subject.tokenValue = 'any'; const { updateLabelTokenColorSpy } = setupSpies(subject); subject.render(tokenValueContainer, tokenValueElement); diff --git a/spec/javascripts/fixtures/ajax_loading_spinner.html.haml b/spec/javascripts/fixtures/ajax_loading_spinner.html.haml deleted file mode 100644 index 09d8c9df3b2..00000000000 --- a/spec/javascripts/fixtures/ajax_loading_spinner.html.haml +++ /dev/null @@ -1,2 +0,0 @@ -%a.js-ajax-loading-spinner{href: "http://goesnowhere.nothing/whereami", data: {remote: true}} - %i.fa.fa-trash-o diff --git a/spec/javascripts/fixtures/autocomplete_sources.rb b/spec/javascripts/fixtures/autocomplete_sources.rb new file mode 100644 index 00000000000..c117fb7cd24 --- /dev/null +++ b/spec/javascripts/fixtures/autocomplete_sources.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Projects::AutocompleteSourcesController, '(JavaScript fixtures)', type: :controller do + include JavaScriptFixturesHelpers + + set(:admin) { create(:admin) } + set(:group) { create(:group, name: 'frontend-fixtures') } + set(:project) { create(:project, namespace: group, path: 'autocomplete-sources-project') } + set(:issue) { create(:issue, project: project) } + + before(:all) do + clean_frontend_fixtures('autocomplete_sources/') + end + + before do + sign_in(admin) + end + + it 'autocomplete_sources/labels.json' do |example| + issue.labels << create(:label, project: project, title: 'bug') + issue.labels << create(:label, project: project, title: 'critical') + + create(:label, project: project, title: 'feature') + create(:label, project: project, title: 'documentation') + + get :labels, + format: :json, + params: { + namespace_id: group.path, + project_id: project.path, + type: issue.class.name, + type_id: issue.id + } + + expect(response).to be_success + store_frontend_fixture(response, example.description) + end +end diff --git a/spec/javascripts/fixtures/balsamiq_viewer.html.haml b/spec/javascripts/fixtures/balsamiq_viewer.html.haml deleted file mode 100644 index 18166ba4901..00000000000 --- a/spec/javascripts/fixtures/balsamiq_viewer.html.haml +++ /dev/null @@ -1 +0,0 @@ -.file-content.balsamiq-viewer#js-balsamiq-viewer{ data: { endpoint: '/test' } } diff --git a/spec/javascripts/fixtures/create_item_dropdown.html.haml b/spec/javascripts/fixtures/create_item_dropdown.html.haml deleted file mode 100644 index d4d91b93caf..00000000000 --- a/spec/javascripts/fixtures/create_item_dropdown.html.haml +++ /dev/null @@ -1,13 +0,0 @@ -.js-create-item-dropdown-fixture-root - %input{ name: 'variable[environment]', type: 'hidden' } - = dropdown_tag('some label', - options: { toggle_class: 'js-dropdown-menu-toggle', - content_class: 'js-dropdown-content', - filter: true, - dropdown_class: "dropdown-menu-selectable", - footer_content: true }) do - %ul.dropdown-footer-list - %li - %button{ class: "dropdown-create-new-item-button js-dropdown-create-new-item" } - Create wildcard - %code diff --git a/spec/javascripts/fixtures/emojis.rb b/spec/javascripts/fixtures/emojis.rb deleted file mode 100644 index 4dab697e5e2..00000000000 --- a/spec/javascripts/fixtures/emojis.rb +++ /dev/null @@ -1,20 +0,0 @@ -require 'spec_helper' - -describe 'Emojis (JavaScript fixtures)' do - include JavaScriptFixturesHelpers - - before(:all) do - clean_frontend_fixtures('emojis/') - end - - it 'emojis/emojis.json' do |example| - JavaScriptFixturesHelpers::FIXTURE_PATHS.each do |fixture_path| - next unless File.directory?(fixture_path) - - # Copying the emojis.json from the public folder - fixture_file_name = File.expand_path('emojis/emojis.json', fixture_path) - FileUtils.mkdir_p(File.dirname(fixture_file_name)) - FileUtils.cp(Rails.root.join('public/-/emojis/1/emojis.json'), fixture_file_name) - end - end -end diff --git a/spec/javascripts/fixtures/event_filter.html.haml b/spec/javascripts/fixtures/event_filter.html.haml deleted file mode 100644 index aa7af61c7eb..00000000000 --- a/spec/javascripts/fixtures/event_filter.html.haml +++ /dev/null @@ -1,25 +0,0 @@ -%ul.nav-links.event-filter.scrolling-tabs.nav.nav-tabs - %li.active - %a.event-filter-link{ id: "all_event_filter", title: "Filter by all", href: "/dashboard/activity"} - %span - All - %li - %a.event-filter-link{ id: "push_event_filter", title: "Filter by push events", href: "/dashboard/activity"} - %span - Push events - %li - %a.event-filter-link{ id: "merged_event_filter", title: "Filter by merge events", href: "/dashboard/activity"} - %span - Merge events - %li - %a.event-filter-link{ id: "issue_event_filter", title: "Filter by issue events", href: "/dashboard/activity"} - %span - Issue events - %li - %a.event-filter-link{ id: "comments_event_filter", title: "Filter by comments", href: "/dashboard/activity"} - %span - Comments - %li - %a.event-filter-link{ id: "team_event_filter", title: "Filter by team", href: "/dashboard/activity"} - %span - Team
\ No newline at end of file diff --git a/spec/javascripts/fixtures/gl_dropdown.html.haml b/spec/javascripts/fixtures/gl_dropdown.html.haml deleted file mode 100644 index 43d57c2c4dc..00000000000 --- a/spec/javascripts/fixtures/gl_dropdown.html.haml +++ /dev/null @@ -1,17 +0,0 @@ -%div - .dropdown.inline - %button#js-project-dropdown.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}} - .dropdown-toggle-text - Projects - %i.fa.fa-chevron-down.dropdown-toggle-caret.js-projects-dropdown-toggle - .dropdown-menu.dropdown-select.dropdown-menu-selectable - .dropdown-title - %span Go to project - %button.dropdown-title-button.dropdown-menu-close{aria: {label: 'Close'}} - %i.fa.fa-times.dropdown-menu-close-icon - .dropdown-input - %input.dropdown-input-field{type: 'search', placeholder: 'Filter results'} - %i.fa.fa-search.dropdown-input-search - .dropdown-content - .dropdown-loading - %i.fa.fa-spinner.fa-spin diff --git a/spec/javascripts/fixtures/gl_field_errors.html.haml b/spec/javascripts/fixtures/gl_field_errors.html.haml deleted file mode 100644 index 69445b61367..00000000000 --- a/spec/javascripts/fixtures/gl_field_errors.html.haml +++ /dev/null @@ -1,15 +0,0 @@ -%form.gl-show-field-errors{action: 'submit', method: 'post'} - .form-group - %input.required-text{required: true, type: 'text'} Text - .form-group - %input.email{type: 'email', title: 'Please provide a valid email address.', required: true } Email - .form-group - %input.password{type: 'password', required: true} Password - .form-group - %input.alphanumeric{type: 'text', pattern: '[a-zA-Z0-9]', required: true} Alphanumeric - .form-group - %input.hidden{ type:'hidden' } - .form-group - %input.custom.gl-field-error-ignore{ type:'text' } Custom, do not validate - .form-group - %input.submit{type: 'submit'} Submit diff --git a/spec/javascripts/fixtures/issuable_filter.html.haml b/spec/javascripts/fixtures/issuable_filter.html.haml deleted file mode 100644 index 84fa5395cb8..00000000000 --- a/spec/javascripts/fixtures/issuable_filter.html.haml +++ /dev/null @@ -1,8 +0,0 @@ -%form.js-filter-form{action: '/user/project/issues?scope=all&state=closed'} - %input{id: 'utf8', name: 'utf8', value: '✓'} - %input{id: 'check-all-issues', name: 'check-all-issues'} - %input{id: 'search', name: 'search'} - %input{id: 'author_id', name: 'author_id'} - %input{id: 'assignee_id', name: 'assignee_id'} - %input{id: 'milestone_title', name: 'milestone_title'} - %input{id: 'label_name', name: 'label_name'} diff --git a/spec/javascripts/fixtures/issue_sidebar_label.html.haml b/spec/javascripts/fixtures/issue_sidebar_label.html.haml deleted file mode 100644 index 06ce248dc9c..00000000000 --- a/spec/javascripts/fixtures/issue_sidebar_label.html.haml +++ /dev/null @@ -1,16 +0,0 @@ -.block.labels - .sidebar-collapsed-icon.js-sidebar-labels-tooltip - .title.hide-collapsed - %a.edit-link.float-right{ href: "#" } - Edit - .selectbox.hide-collapsed{ style: "display: none;" } - .dropdown - %button.dropdown-menu-toggle.js-label-select.js-multiselect{ type: "button", data: { ability_name: "issue", field_name: "issue[label_names][]", issue_update: "/root/test/issues/2.json", labels: "/root/test/labels.json", project_id: "12", show_any: "true", show_no: "true", toggle: "dropdown" } } - %span.dropdown-toggle-text - Label - %i.fa.fa-chevron-down - .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable - .dropdown-page-one - .dropdown-content - .dropdown-loading - %i.fa.fa-spinner.fa-spin diff --git a/spec/javascripts/fixtures/line_highlighter.html.haml b/spec/javascripts/fixtures/line_highlighter.html.haml deleted file mode 100644 index 2782c50e298..00000000000 --- a/spec/javascripts/fixtures/line_highlighter.html.haml +++ /dev/null @@ -1,11 +0,0 @@ -.file-holder - .file-content - .line-numbers - - 1.upto(25) do |i| - %a{href: "#L#{i}", id: "L#{i}", 'data-line-number' => i} - %i.fa.fa-link - = i - %pre.code.highlight - %code - - 1.upto(25) do |i| - %span.line{id: "LC#{i}"}= "Line #{i}" diff --git a/spec/javascripts/fixtures/linked_tabs.html.haml b/spec/javascripts/fixtures/linked_tabs.html.haml deleted file mode 100644 index 632606e0536..00000000000 --- a/spec/javascripts/fixtures/linked_tabs.html.haml +++ /dev/null @@ -1,13 +0,0 @@ -%ul.nav.nav-tabs.new-session-tabs.linked-tabs - %li.nav-item - %a.nav-link{ href: 'foo/bar/1', data: { target: 'div#tab1', action: 'tab1', toggle: 'tab' } } - Tab 1 - %li.nav-item - %a.nav-link{ href: 'foo/bar/1/context', data: { target: 'div#tab2', action: 'tab2', toggle: 'tab' } } - Tab 2 - -.tab-content - #tab1.tab-pane - Tab 1 Content - #tab2.tab-pane - Tab 2 Content diff --git a/spec/javascripts/fixtures/merge_requests_show.html.haml b/spec/javascripts/fixtures/merge_requests_show.html.haml deleted file mode 100644 index 8447dfdda32..00000000000 --- a/spec/javascripts/fixtures/merge_requests_show.html.haml +++ /dev/null @@ -1,13 +0,0 @@ -%a.btn-close - -.detail-page-description - .description.js-task-list-container - .wiki - %ul.task-list - %li.task-list-item - %input.task-list-item-checkbox{type: 'checkbox'} - Task List Item - %textarea.js-task-list-field - \- [ ] Task List Item - -%form.js-issuable-update{action: '/foo'} diff --git a/spec/javascripts/fixtures/mini_dropdown_graph.html.haml b/spec/javascripts/fixtures/mini_dropdown_graph.html.haml deleted file mode 100644 index 74584993739..00000000000 --- a/spec/javascripts/fixtures/mini_dropdown_graph.html.haml +++ /dev/null @@ -1,10 +0,0 @@ -%div.js-builds-dropdown-tests.dropdown.dropdown.js-mini-pipeline-graph - %button.js-builds-dropdown-button{'data-stage-endpoint' => 'foobar', data: { toggle: 'dropdown'} } - Dropdown - - %ul.dropdown-menu.mini-pipeline-graph-dropdown-menu.js-builds-dropdown-container - %li.js-builds-dropdown-list.scrollable-menu - %ul - - %li.js-builds-dropdown-loading.hidden - %span.fa.fa-spinner diff --git a/spec/javascripts/fixtures/notebook_viewer.html.haml b/spec/javascripts/fixtures/notebook_viewer.html.haml deleted file mode 100644 index 17a7a9d8f31..00000000000 --- a/spec/javascripts/fixtures/notebook_viewer.html.haml +++ /dev/null @@ -1 +0,0 @@ -.file-content#js-notebook-viewer{ data: { endpoint: '/test' } } diff --git a/spec/javascripts/fixtures/oauth_remember_me.html.haml b/spec/javascripts/fixtures/oauth_remember_me.html.haml deleted file mode 100644 index a5d7c4e816a..00000000000 --- a/spec/javascripts/fixtures/oauth_remember_me.html.haml +++ /dev/null @@ -1,6 +0,0 @@ -#oauth-container - %input#remember_me{ type: "checkbox" } - - %a.oauth-login.twitter{ href: "http://example.com/" } - %a.oauth-login.github{ href: "http://example.com/" } - %a.oauth-login.facebook{ href: "http://example.com/?redirect_fragment=L1" } diff --git a/spec/javascripts/fixtures/pdf_viewer.html.haml b/spec/javascripts/fixtures/pdf_viewer.html.haml deleted file mode 100644 index 2e57beae54b..00000000000 --- a/spec/javascripts/fixtures/pdf_viewer.html.haml +++ /dev/null @@ -1 +0,0 @@ -.file-content#js-pdf-viewer{ data: { endpoint: '/test' } } diff --git a/spec/javascripts/fixtures/pipeline_graph.html.haml b/spec/javascripts/fixtures/pipeline_graph.html.haml deleted file mode 100644 index c0b5ab4411e..00000000000 --- a/spec/javascripts/fixtures/pipeline_graph.html.haml +++ /dev/null @@ -1,14 +0,0 @@ -%div.pipeline-visualization.js-pipeline-graph - %ul.stage-column-list - %li.stage-column - .stage-name - %a{:href => "/"} - Test - .builds-container - %ul - %li.build - .curve - %a - %svg - .ci-status-text - stop_review diff --git a/spec/javascripts/fixtures/pipelines.html.haml b/spec/javascripts/fixtures/pipelines.html.haml deleted file mode 100644 index 0161c0550d1..00000000000 --- a/spec/javascripts/fixtures/pipelines.html.haml +++ /dev/null @@ -1,12 +0,0 @@ -%div - #pipelines-list-vue{ data: { endpoint: 'foo', - "help-page-path" => 'foo', - "help-auto-devops-path" => 'foo', - "empty-state-svg-path" => 'foo', - "error-state-svg-path" => 'foo', - "new-pipeline-path" => 'foo', - "can-create-pipeline" => 'true', - "has-ci" => 'foo', - "ci-lint-path" => 'foo', - "reset-cache-path" => 'foo' } } - diff --git a/spec/javascripts/fixtures/project_select_combo_button.html.haml b/spec/javascripts/fixtures/project_select_combo_button.html.haml deleted file mode 100644 index 432cd5fcc74..00000000000 --- a/spec/javascripts/fixtures/project_select_combo_button.html.haml +++ /dev/null @@ -1,6 +0,0 @@ -.project-item-select-holder - %input.project-item-select{ data: { group_id: '12345' , relative_path: 'issues/new' } } - %a.new-project-item-link{ data: { label: 'New issue', type: 'issues' }, href: ''} - %i.fa.fa-spinner.spin - %a.new-project-item-select-button - %i.fa.fa-caret-down diff --git a/spec/javascripts/fixtures/search_autocomplete.html.haml b/spec/javascripts/fixtures/search_autocomplete.html.haml deleted file mode 100644 index 4aa54da9411..00000000000 --- a/spec/javascripts/fixtures/search_autocomplete.html.haml +++ /dev/null @@ -1,9 +0,0 @@ -.search.search-form - %form.form-inline - .search-input-container - .search-input-wrap - .dropdown - %input#search.search-input.dropdown-menu-toggle - .dropdown-menu.dropdown-select - .dropdown-content - %input{ type: "hidden", class: "js-search-project-options" } diff --git a/spec/javascripts/fixtures/signin_tabs.html.haml b/spec/javascripts/fixtures/signin_tabs.html.haml deleted file mode 100644 index 2e00fe7865e..00000000000 --- a/spec/javascripts/fixtures/signin_tabs.html.haml +++ /dev/null @@ -1,5 +0,0 @@ -%ul.nav-links.new-session-tabs - %li.active - %a{ href: '#ldap' } LDAP - %li - %a{ href: '#login-pane'} Standard diff --git a/spec/javascripts/fixtures/sketch_viewer.html.haml b/spec/javascripts/fixtures/sketch_viewer.html.haml deleted file mode 100644 index f01bd00925a..00000000000 --- a/spec/javascripts/fixtures/sketch_viewer.html.haml +++ /dev/null @@ -1,2 +0,0 @@ -.file-content#js-sketch-viewer{ data: { endpoint: '/test_sketch_file.sketch' } } - .js-loading-icon diff --git a/spec/javascripts/fixtures/static/README.md b/spec/javascripts/fixtures/static/README.md new file mode 100644 index 00000000000..b5c2f8233bf --- /dev/null +++ b/spec/javascripts/fixtures/static/README.md @@ -0,0 +1,3 @@ +# Please do not add new files here! + +Instead use a Ruby file in the fixtures root directory (`spec/javascripts/fixtures/`). diff --git a/spec/javascripts/fixtures/static/ajax_loading_spinner.html.raw b/spec/javascripts/fixtures/static/ajax_loading_spinner.html.raw new file mode 100644 index 00000000000..0e1ebb32b1c --- /dev/null +++ b/spec/javascripts/fixtures/static/ajax_loading_spinner.html.raw @@ -0,0 +1,3 @@ +<a class="js-ajax-loading-spinner" data-remote href="http://goesnowhere.nothing/whereami"> +<i class="fa fa-trash-o"></i> +</a> diff --git a/spec/javascripts/fixtures/static/balsamiq_viewer.html.raw b/spec/javascripts/fixtures/static/balsamiq_viewer.html.raw new file mode 100644 index 00000000000..cdd723d1a84 --- /dev/null +++ b/spec/javascripts/fixtures/static/balsamiq_viewer.html.raw @@ -0,0 +1 @@ +<div class="file-content balsamiq-viewer" data-endpoint="/test" id="js-balsamiq-viewer"></div> diff --git a/spec/javascripts/fixtures/static/create_item_dropdown.html.raw b/spec/javascripts/fixtures/static/create_item_dropdown.html.raw new file mode 100644 index 00000000000..d2d38370092 --- /dev/null +++ b/spec/javascripts/fixtures/static/create_item_dropdown.html.raw @@ -0,0 +1,11 @@ +<div class="js-create-item-dropdown-fixture-root"> +<input name="variable[environment]" type="hidden"> +<div class="dropdown "><button class="dropdown-menu-toggle js-dropdown-menu-toggle" type="button" data-toggle="dropdown"><span class="dropdown-toggle-text ">some label</span><i aria-hidden="true" data-hidden="true" class="fa fa-chevron-down"></i></button><div class="dropdown-menu dropdown-select dropdown-menu-selectable"><div class="dropdown-input"><input type="search" id="" class="dropdown-input-field" autocomplete="off" /><i aria-hidden="true" data-hidden="true" class="fa fa-search dropdown-input-search"></i><i aria-hidden="true" data-hidden="true" role="button" class="fa fa-times dropdown-input-clear js-dropdown-input-clear"></i></div><div class="dropdown-content js-dropdown-content"></div><div class="dropdown-footer"><ul class="dropdown-footer-list"> +<li> +<button class="dropdown-create-new-item-button js-dropdown-create-new-item"> +Create wildcard +<code></code> +</button> +</li> +</ul> +</div><div class="dropdown-loading"><i aria-hidden="true" data-hidden="true" class="fa fa-spinner fa-spin"></i></div></div></div></div> diff --git a/spec/javascripts/fixtures/static/event_filter.html.raw b/spec/javascripts/fixtures/static/event_filter.html.raw new file mode 100644 index 00000000000..8e9b6fb1b5c --- /dev/null +++ b/spec/javascripts/fixtures/static/event_filter.html.raw @@ -0,0 +1,44 @@ +<ul class="nav-links event-filter scrolling-tabs nav nav-tabs"> +<li class="active"> +<a class="event-filter-link" href="/dashboard/activity" id="all_event_filter" title="Filter by all"> +<span> +All +</span> +</a> +</li> +<li> +<a class="event-filter-link" href="/dashboard/activity" id="push_event_filter" title="Filter by push events"> +<span> +Push events +</span> +</a> +</li> +<li> +<a class="event-filter-link" href="/dashboard/activity" id="merged_event_filter" title="Filter by merge events"> +<span> +Merge events +</span> +</a> +</li> +<li> +<a class="event-filter-link" href="/dashboard/activity" id="issue_event_filter" title="Filter by issue events"> +<span> +Issue events +</span> +</a> +</li> +<li> +<a class="event-filter-link" href="/dashboard/activity" id="comments_event_filter" title="Filter by comments"> +<span> +Comments +</span> +</a> +</li> +<li> +<a class="event-filter-link" href="/dashboard/activity" id="team_event_filter" title="Filter by team"> +<span> +Team +</span> +</a> +</li> +</ul> diff --git a/spec/javascripts/fixtures/static/gl_dropdown.html.raw b/spec/javascripts/fixtures/static/gl_dropdown.html.raw new file mode 100644 index 00000000000..08f6738414e --- /dev/null +++ b/spec/javascripts/fixtures/static/gl_dropdown.html.raw @@ -0,0 +1,26 @@ +<div> +<div class="dropdown inline"> +<button class="dropdown-menu-toggle" data-toggle="dropdown" id="js-project-dropdown" type="button"> +<div class="dropdown-toggle-text"> +Projects +</div> +<i class="fa fa-chevron-down dropdown-toggle-caret js-projects-dropdown-toggle"></i> +</button> +<div class="dropdown-menu dropdown-select dropdown-menu-selectable"> +<div class="dropdown-title"> +<span>Go to project</span> +<button aria="{:label=>"Close"}" class="dropdown-title-button dropdown-menu-close"> +<i class="fa fa-times dropdown-menu-close-icon"></i> +</button> +</div> +<div class="dropdown-input"> +<input class="dropdown-input-field" placeholder="Filter results" type="search"> +<i class="fa fa-search dropdown-input-search"></i> +</div> +<div class="dropdown-content"></div> +<div class="dropdown-loading"> +<i class="fa fa-spinner fa-spin"></i> +</div> +</div> +</div> +</div> diff --git a/spec/javascripts/fixtures/static/gl_field_errors.html.raw b/spec/javascripts/fixtures/static/gl_field_errors.html.raw new file mode 100644 index 00000000000..f8470e02b7c --- /dev/null +++ b/spec/javascripts/fixtures/static/gl_field_errors.html.raw @@ -0,0 +1,22 @@ +<form action="submit" class="gl-show-field-errors" method="post"> +<div class="form-group"> +<input class="required-text" required type="text">Text</input> +</div> +<div class="form-group"> +<input class="email" required title="Please provide a valid email address." type="email">Email</input> +</div> +<div class="form-group"> +<input class="password" required type="password">Password</input> +</div> +<div class="form-group"> +<input class="alphanumeric" pattern="[a-zA-Z0-9]" required type="text">Alphanumeric</input> +</div> +<div class="form-group"> +<input class="hidden" type="hidden"> +</div> +<div class="form-group"> +<input class="custom gl-field-error-ignore" type="text">Custom, do not validate</input> +</div> +<div class="form-group"></div> +<input class="submit" type="submit">Submit</input> +</form> diff --git a/spec/javascripts/fixtures/static/issuable_filter.html.raw b/spec/javascripts/fixtures/static/issuable_filter.html.raw new file mode 100644 index 00000000000..06b70fb43f1 --- /dev/null +++ b/spec/javascripts/fixtures/static/issuable_filter.html.raw @@ -0,0 +1,9 @@ +<form action="/user/project/issues?scope=all&state=closed" class="js-filter-form"> +<input id="utf8" name="utf8" value="✓"> +<input id="check-all-issues" name="check-all-issues"> +<input id="search" name="search"> +<input id="author_id" name="author_id"> +<input id="assignee_id" name="assignee_id"> +<input id="milestone_title" name="milestone_title"> +<input id="label_name" name="label_name"> +</form> diff --git a/spec/javascripts/fixtures/static/issue_sidebar_label.html.raw b/spec/javascripts/fixtures/static/issue_sidebar_label.html.raw new file mode 100644 index 00000000000..ec8fb30f219 --- /dev/null +++ b/spec/javascripts/fixtures/static/issue_sidebar_label.html.raw @@ -0,0 +1,26 @@ +<div class="block labels"> +<div class="sidebar-collapsed-icon js-sidebar-labels-tooltip"></div> +<div class="title hide-collapsed"> +<a class="edit-link float-right" href="#"> +Edit +</a> +</div> +<div class="selectbox hide-collapsed" style="display: none;"> +<div class="dropdown"> +<button class="dropdown-menu-toggle js-label-select js-multiselect" data-ability-name="issue" data-field-name="issue[label_names][]" data-issue-update="/root/test/issues/2.json" data-labels="/root/test/labels.json" data-project-id="12" data-show-any="true" data-show-no="true" data-toggle="dropdown" type="button"> +<span class="dropdown-toggle-text"> +Label +</span> +<i class="fa fa-chevron-down"></i> +</button> +<div class="dropdown-menu dropdown-select dropdown-menu-paging dropdown-menu-labels dropdown-menu-selectable"> +<div class="dropdown-page-one"> +<div class="dropdown-content"></div> +<div class="dropdown-loading"> +<i class="fa fa-spinner fa-spin"></i> +</div> +</div> +</div> +</div> +</div> +</div> diff --git a/spec/javascripts/fixtures/static/line_highlighter.html.raw b/spec/javascripts/fixtures/static/line_highlighter.html.raw new file mode 100644 index 00000000000..897a25d6760 --- /dev/null +++ b/spec/javascripts/fixtures/static/line_highlighter.html.raw @@ -0,0 +1,107 @@ +<div class="file-holder"> +<div class="file-content"> +<div class="line-numbers"> +<a data-line-number="1" href="#L1" id="L1"> +<i class="fa fa-link"></i> +1 +</a> +<a data-line-number="2" href="#L2" id="L2"> +<i class="fa fa-link"></i> +2 +</a> +<a data-line-number="3" href="#L3" id="L3"> +<i class="fa fa-link"></i> +3 +</a> +<a data-line-number="4" href="#L4" id="L4"> +<i class="fa fa-link"></i> +4 +</a> +<a data-line-number="5" href="#L5" id="L5"> +<i class="fa fa-link"></i> +5 +</a> +<a data-line-number="6" href="#L6" id="L6"> +<i class="fa fa-link"></i> +6 +</a> +<a data-line-number="7" href="#L7" id="L7"> +<i class="fa fa-link"></i> +7 +</a> +<a data-line-number="8" href="#L8" id="L8"> +<i class="fa fa-link"></i> +8 +</a> +<a data-line-number="9" href="#L9" id="L9"> +<i class="fa fa-link"></i> +9 +</a> +<a data-line-number="10" href="#L10" id="L10"> +<i class="fa fa-link"></i> +10 +</a> +<a data-line-number="11" href="#L11" id="L11"> +<i class="fa fa-link"></i> +11 +</a> +<a data-line-number="12" href="#L12" id="L12"> +<i class="fa fa-link"></i> +12 +</a> +<a data-line-number="13" href="#L13" id="L13"> +<i class="fa fa-link"></i> +13 +</a> +<a data-line-number="14" href="#L14" id="L14"> +<i class="fa fa-link"></i> +14 +</a> +<a data-line-number="15" href="#L15" id="L15"> +<i class="fa fa-link"></i> +15 +</a> +<a data-line-number="16" href="#L16" id="L16"> +<i class="fa fa-link"></i> +16 +</a> +<a data-line-number="17" href="#L17" id="L17"> +<i class="fa fa-link"></i> +17 +</a> +<a data-line-number="18" href="#L18" id="L18"> +<i class="fa fa-link"></i> +18 +</a> +<a data-line-number="19" href="#L19" id="L19"> +<i class="fa fa-link"></i> +19 +</a> +<a data-line-number="20" href="#L20" id="L20"> +<i class="fa fa-link"></i> +20 +</a> +<a data-line-number="21" href="#L21" id="L21"> +<i class="fa fa-link"></i> +21 +</a> +<a data-line-number="22" href="#L22" id="L22"> +<i class="fa fa-link"></i> +22 +</a> +<a data-line-number="23" href="#L23" id="L23"> +<i class="fa fa-link"></i> +23 +</a> +<a data-line-number="24" href="#L24" id="L24"> +<i class="fa fa-link"></i> +24 +</a> +<a data-line-number="25" href="#L25" id="L25"> +<i class="fa fa-link"></i> +25 +</a> +</div> +<pre class="code highlight"><code><span class="line" id="LC1">Line 1</span><span class="line" id="LC2">Line 2</span><span class="line" id="LC3">Line 3</span><span class="line" id="LC4">Line 4</span><span class="line" id="LC5">Line 5</span><span class="line" id="LC6">Line 6</span><span class="line" id="LC7">Line 7</span><span class="line" id="LC8">Line 8</span><span class="line" id="LC9">Line 9</span><span class="line" id="LC10">Line 10</span><span class="line" id="LC11">Line 11</span><span class="line" id="LC12">Line 12</span><span class="line" id="LC13">Line 13</span><span class="line" id="LC14">Line 14</span><span class="line" id="LC15">Line 15</span><span class="line" id="LC16">Line 16</span><span class="line" id="LC17">Line 17</span><span class="line" id="LC18">Line 18</span><span class="line" id="LC19">Line 19</span><span class="line" id="LC20">Line 20</span><span class="line" id="LC21">Line 21</span><span class="line" id="LC22">Line 22</span><span class="line" id="LC23">Line 23</span><span class="line" id="LC24">Line 24</span><span class="line" id="LC25">Line 25</span></code></pre> +</div> +</div> diff --git a/spec/javascripts/fixtures/static/linked_tabs.html.raw b/spec/javascripts/fixtures/static/linked_tabs.html.raw new file mode 100644 index 00000000000..c25463bf1db --- /dev/null +++ b/spec/javascripts/fixtures/static/linked_tabs.html.raw @@ -0,0 +1,20 @@ +<ul class="nav nav-tabs new-session-tabs linked-tabs"> +<li class="nav-item"> +<a class="nav-link" data-action="tab1" data-target="div#tab1" data-toggle="tab" href="foo/bar/1"> +Tab 1 +</a> +</li> +<li class="nav-item"> +<a class="nav-link" data-action="tab2" data-target="div#tab2" data-toggle="tab" href="foo/bar/1/context"> +Tab 2 +</a> +</li> +</ul> +<div class="tab-content"> +<div class="tab-pane" id="tab1"> +Tab 1 Content +</div> +<div class="tab-pane" id="tab2"> +Tab 2 Content +</div> +</div> diff --git a/spec/javascripts/fixtures/static/merge_requests_show.html.raw b/spec/javascripts/fixtures/static/merge_requests_show.html.raw new file mode 100644 index 00000000000..e219d9462aa --- /dev/null +++ b/spec/javascripts/fixtures/static/merge_requests_show.html.raw @@ -0,0 +1,15 @@ +<a class="btn-close"></a> +<div class="detail-page-description"> +<div class="description js-task-list-container"> +<div class="wiki"> +<ul class="task-list"> +<li class="task-list-item"> +<input class="task-list-item-checkbox" type="checkbox"> +Task List Item +</li> +</ul> +<textarea class="js-task-list-field">- [ ] Task List Item</textarea> +</div> +</div> +</div> +<form action="/foo" class="js-issuable-update"></form> diff --git a/spec/javascripts/fixtures/static/mini_dropdown_graph.html.raw b/spec/javascripts/fixtures/static/mini_dropdown_graph.html.raw new file mode 100644 index 00000000000..cd0b8dec3fc --- /dev/null +++ b/spec/javascripts/fixtures/static/mini_dropdown_graph.html.raw @@ -0,0 +1,13 @@ +<div class="js-builds-dropdown-tests dropdown dropdown js-mini-pipeline-graph"> +<button class="js-builds-dropdown-button" data-toggle="dropdown" data-stage-endpoint="foobar"> +Dropdown +</button> +<ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container"> +<li class="js-builds-dropdown-list scrollable-menu"> +<ul></ul> +</li> +<li class="js-builds-dropdown-loading hidden"> +<span class="fa fa-spinner"></span> +</li> +</ul> +</div> diff --git a/spec/javascripts/fixtures/static/notebook_viewer.html.raw b/spec/javascripts/fixtures/static/notebook_viewer.html.raw new file mode 100644 index 00000000000..4bbb7bf1094 --- /dev/null +++ b/spec/javascripts/fixtures/static/notebook_viewer.html.raw @@ -0,0 +1 @@ +<div class="file-content" data-endpoint="/test" id="js-notebook-viewer"></div> diff --git a/spec/javascripts/fixtures/static/oauth_remember_me.html.raw b/spec/javascripts/fixtures/static/oauth_remember_me.html.raw new file mode 100644 index 00000000000..9ba1ffc72fe --- /dev/null +++ b/spec/javascripts/fixtures/static/oauth_remember_me.html.raw @@ -0,0 +1,6 @@ +<div id="oauth-container"> +<input id="remember_me" type="checkbox"> +<a class="oauth-login twitter" href="http://example.com/"></a> +<a class="oauth-login github" href="http://example.com/"></a> +<a class="oauth-login facebook" href="http://example.com/?redirect_fragment=L1"></a> +</div> diff --git a/spec/javascripts/fixtures/static/pdf_viewer.html.raw b/spec/javascripts/fixtures/static/pdf_viewer.html.raw new file mode 100644 index 00000000000..350d35a262f --- /dev/null +++ b/spec/javascripts/fixtures/static/pdf_viewer.html.raw @@ -0,0 +1 @@ +<div class="file-content" data-endpoint="/test" id="js-pdf-viewer"></div> diff --git a/spec/javascripts/fixtures/static/pipeline_graph.html.raw b/spec/javascripts/fixtures/static/pipeline_graph.html.raw new file mode 100644 index 00000000000..422372bb7d5 --- /dev/null +++ b/spec/javascripts/fixtures/static/pipeline_graph.html.raw @@ -0,0 +1,24 @@ +<div class="pipeline-visualization js-pipeline-graph"> +<ul class="stage-column-list"> +<li class="stage-column"> +<div class="stage-name"> +<a href="/"> +Test +<div class="builds-container"> +<ul> +<li class="build"> +<div class="curve"></div> +<a> +<svg></svg> +<div class="ci-status-text"> +stop_review +</div> +</a> +</li> +</ul> +</div> +</a> +</div> +</li> +</ul> +</div> diff --git a/spec/javascripts/fixtures/static/pipelines.html.raw b/spec/javascripts/fixtures/static/pipelines.html.raw new file mode 100644 index 00000000000..42333f94f2f --- /dev/null +++ b/spec/javascripts/fixtures/static/pipelines.html.raw @@ -0,0 +1,3 @@ +<div> +<div data-can-create-pipeline="true" data-ci-lint-path="foo" data-empty-state-svg-path="foo" data-endpoint="foo" data-error-state-svg-path="foo" data-has-ci="foo" data-help-auto-devops-path="foo" data-help-page-path="foo" data-new-pipeline-path="foo" data-reset-cache-path="foo" id="pipelines-list-vue"></div> +</div> diff --git a/spec/javascripts/fixtures/static/project_select_combo_button.html.raw b/spec/javascripts/fixtures/static/project_select_combo_button.html.raw new file mode 100644 index 00000000000..50c826051c0 --- /dev/null +++ b/spec/javascripts/fixtures/static/project_select_combo_button.html.raw @@ -0,0 +1,9 @@ +<div class="project-item-select-holder"> +<input class="project-item-select" data-group-id="12345" data-relative-path="issues/new"> +<a class="new-project-item-link" data-label="New issue" data-type="issues" href=""> +<i class="fa fa-spinner spin"></i> +</a> +<a class="new-project-item-select-button"> +<i class="fa fa-caret-down"></i> +</a> +</div> diff --git a/spec/javascripts/fixtures/static/search_autocomplete.html.raw b/spec/javascripts/fixtures/static/search_autocomplete.html.raw new file mode 100644 index 00000000000..29db9020424 --- /dev/null +++ b/spec/javascripts/fixtures/static/search_autocomplete.html.raw @@ -0,0 +1,15 @@ +<div class="search search-form"> +<form class="form-inline"> +<div class="search-input-container"> +<div class="search-input-wrap"> +<div class="dropdown"> +<input class="search-input dropdown-menu-toggle" id="search"> +<div class="dropdown-menu dropdown-select"> +<div class="dropdown-content"></div> +</div> +</div> +</div> +</div> +<input class="js-search-project-options" type="hidden"> +</form> +</div> diff --git a/spec/javascripts/fixtures/static/signin_tabs.html.raw b/spec/javascripts/fixtures/static/signin_tabs.html.raw new file mode 100644 index 00000000000..7e66ab9394b --- /dev/null +++ b/spec/javascripts/fixtures/static/signin_tabs.html.raw @@ -0,0 +1,8 @@ +<ul class="nav-links new-session-tabs"> +<li class="active"> +<a href="#ldap">LDAP</a> +</li> +<li> +<a href="#login-pane">Standard</a> +</li> +</ul> diff --git a/spec/javascripts/fixtures/static/sketch_viewer.html.raw b/spec/javascripts/fixtures/static/sketch_viewer.html.raw new file mode 100644 index 00000000000..e25e554e568 --- /dev/null +++ b/spec/javascripts/fixtures/static/sketch_viewer.html.raw @@ -0,0 +1,3 @@ +<div class="file-content" data-endpoint="/test_sketch_file.sketch" id="js-sketch-viewer"> +<div class="js-loading-icon"></div> +</div> diff --git a/spec/javascripts/fixtures/static_fixtures.rb b/spec/javascripts/fixtures/static_fixtures.rb index 852a82587b9..b5188eeb994 100644 --- a/spec/javascripts/fixtures/static_fixtures.rb +++ b/spec/javascripts/fixtures/static_fixtures.rb @@ -3,29 +3,17 @@ require 'spec_helper' describe ApplicationController, '(Static JavaScript fixtures)', type: :controller do include JavaScriptFixturesHelpers - before(:all) do - clean_frontend_fixtures('static/') - end - - JavaScriptFixturesHelpers::FIXTURE_PATHS.each do |fixture_path| - fixtures_path = File.expand_path(fixture_path, Rails.root) - - Dir.glob(File.expand_path('**/*.haml', fixtures_path)).map do |file_path| - template_file_name = file_path.sub(/\A#{fixtures_path}#{File::SEPARATOR}/, '') - - it "static/#{template_file_name.sub(/\.haml\z/, '.raw')}" do |example| - fixture_file_name = example.description - rendered = render_template(fixture_path, template_file_name) - store_frontend_fixture(rendered, fixture_file_name) - end + Dir.glob('{,ee/}spec/javascripts/fixtures/**/*.haml').map do |file_path| + it "static/#{file_path.sub(%r{\A(ee/)?spec/javascripts/fixtures/}, '').sub(/\.haml\z/, '.raw')}" do |example| + store_frontend_fixture(render_template(file_path), example.description) end end private - def render_template(fixture_path, template_file_name) + def render_template(template_file_name) controller = ApplicationController.new - controller.prepend_view_path(fixture_path) - controller.render_to_string(template: template_file_name, layout: false) + controller.prepend_view_path(File.dirname(template_file_name)) + controller.render_to_string(template: File.basename(template_file_name), layout: false) end end diff --git a/spec/javascripts/frequent_items/components/app_spec.js b/spec/javascripts/frequent_items/components/app_spec.js index b1cc4d8dc8d..6814f656f5d 100644 --- a/spec/javascripts/frequent_items/components/app_spec.js +++ b/spec/javascripts/frequent_items/components/app_spec.js @@ -194,7 +194,7 @@ describe('Frequent Items App Component', () => { expect(loadingEl).toBeDefined(); expect(loadingEl.classList.contains('prepend-top-20')).toBe(true); - expect(loadingEl.querySelector('i').getAttribute('aria-label')).toBe('Loading projects'); + expect(loadingEl.querySelector('span').getAttribute('aria-label')).toBe('Loading projects'); done(); }); }); diff --git a/spec/javascripts/groups/components/app_spec.js b/spec/javascripts/groups/components/app_spec.js index d832441dc93..31873311e16 100644 --- a/spec/javascripts/groups/components/app_spec.js +++ b/spec/javascripts/groups/components/app_spec.js @@ -502,7 +502,7 @@ describe('AppComponent', () => { vm.isLoading = true; Vue.nextTick(() => { expect(vm.$el.querySelector('.loading-animation')).toBeDefined(); - expect(vm.$el.querySelector('i.fa').getAttribute('aria-label')).toBe('Loading groups'); + expect(vm.$el.querySelector('span').getAttribute('aria-label')).toBe('Loading groups'); done(); }); }); diff --git a/spec/javascripts/pipelines/graph/stage_column_component_spec.js b/spec/javascripts/pipelines/graph/stage_column_component_spec.js index d0b8f877d6f..dafb892da43 100644 --- a/spec/javascripts/pipelines/graph/stage_column_component_spec.js +++ b/spec/javascripts/pipelines/graph/stage_column_component_spec.js @@ -35,6 +35,7 @@ describe('stage column component', () => { component = mountComponent(StageColumnComponent, { title: 'foo', groups: mockGroups, + hasTriggeredBy: false, }); }); @@ -61,6 +62,7 @@ describe('stage column component', () => { }, ], title: 'test', + hasTriggeredBy: false, }); expect(component.$el.querySelector('.builds-container li').getAttribute('id')).toEqual( diff --git a/spec/javascripts/registry/components/app_spec.js b/spec/javascripts/registry/components/app_spec.js index 67118ac03a5..76a17e6fb31 100644 --- a/spec/javascripts/registry/components/app_spec.js +++ b/spec/javascripts/registry/components/app_spec.js @@ -99,7 +99,7 @@ describe('Registry List', () => { it('should render a loading spinner', done => { Vue.nextTick(() => { - expect(vm.$el.querySelector('.fa-spinner')).not.toBe(null); + expect(vm.$el.querySelector('.spinner')).not.toBe(null); done(); }); }); diff --git a/spec/javascripts/reports/components/grouped_test_reports_app_spec.js b/spec/javascripts/reports/components/grouped_test_reports_app_spec.js index 69767d9cf1c..a17494966a3 100644 --- a/spec/javascripts/reports/components/grouped_test_reports_app_spec.js +++ b/spec/javascripts/reports/components/grouped_test_reports_app_spec.js @@ -61,7 +61,7 @@ describe('Grouped Test Reports App', () => { it('renders success summary text', done => { setTimeout(() => { - expect(vm.$el.querySelector('.fa-spinner')).not.toBeNull(); + expect(vm.$el.querySelector('.spinner')).not.toBeNull(); expect(vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual( 'Test summary results are being parsed', ); @@ -81,7 +81,7 @@ describe('Grouped Test Reports App', () => { it('renders failed summary text + new badge', done => { setTimeout(() => { - expect(vm.$el.querySelector('.fa-spinner')).toBeNull(); + expect(vm.$el.querySelector('.spinner')).toBeNull(); expect(vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual( 'Test summary contained 2 failed test results out of 11 total tests', ); @@ -109,7 +109,7 @@ describe('Grouped Test Reports App', () => { it('renders summary text', done => { setTimeout(() => { - expect(vm.$el.querySelector('.fa-spinner')).toBeNull(); + expect(vm.$el.querySelector('.spinner')).toBeNull(); expect(vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual( 'Test summary contained 2 failed test results and 2 fixed test results out of 11 total tests', ); @@ -137,7 +137,7 @@ describe('Grouped Test Reports App', () => { it('renders summary text', done => { setTimeout(() => { - expect(vm.$el.querySelector('.fa-spinner')).toBeNull(); + expect(vm.$el.querySelector('.spinner')).toBeNull(); expect(vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual( 'Test summary contained 2 fixed test results out of 11 total tests', ); @@ -190,7 +190,7 @@ describe('Grouped Test Reports App', () => { }); it('renders loading summary text with loading icon', done => { - expect(vm.$el.querySelector('.fa-spinner')).not.toBeNull(); + expect(vm.$el.querySelector('.spinner')).not.toBeNull(); expect(vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual( 'Test summary results are being parsed', ); diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_status_icon_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_status_icon_spec.js index a0a336ae604..f622f52a7b9 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_status_icon_spec.js +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_status_icon_spec.js @@ -18,7 +18,7 @@ describe('MR widget status icon component', () => { it('renders loading icon', () => { vm = mountComponent(Component, { status: 'loading' }); - expect(vm.$el.querySelector('.mr-widget-icon i').classList).toContain('fa-spinner'); + expect(vm.$el.querySelector('.mr-widget-icon span').classList).toContain('spinner'); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_auto_merge_failed_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_auto_merge_failed_spec.js index eb4fa0df727..d93badf8cd3 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_auto_merge_failed_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_auto_merge_failed_spec.js @@ -38,7 +38,7 @@ describe('MRWidgetAutoMergeFailed', () => { Vue.nextTick(() => { expect(vm.$el.querySelector('button').getAttribute('disabled')).toEqual('disabled'); - expect(vm.$el.querySelector('button i').classList).toContain('fa-spinner'); + expect(vm.$el.querySelector('button .loading-container span').classList).toContain('spinner'); done(); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_checking_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_checking_spec.js index 7da27bb8890..96e512d222a 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_checking_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_checking_spec.js @@ -20,7 +20,7 @@ describe('MRWidgetChecking', () => { }); it('renders loading icon', () => { - expect(vm.$el.querySelector('.mr-widget-icon i').classList).toContain('fa-spinner'); + expect(vm.$el.querySelector('.mr-widget-icon span').classList).toContain('spinner'); }); it('renders information about merging', () => { diff --git a/spec/javascripts/vue_shared/components/file_icon_spec.js b/spec/javascripts/vue_shared/components/file_icon_spec.js index 34c9b35e02a..5bea8c43da3 100644 --- a/spec/javascripts/vue_shared/components/file_icon_spec.js +++ b/spec/javascripts/vue_shared/components/file_icon_spec.js @@ -70,12 +70,9 @@ describe('File Icon component', () => { loading: true, }); - const { classList } = vm.$el.querySelector('i'); + const { classList } = vm.$el.querySelector('.loading-container span'); - expect(classList.contains('fa')).toEqual(true); - expect(classList.contains('fa-spin')).toEqual(true); - expect(classList.contains('fa-spinner')).toEqual(true); - expect(classList.contains('fa-1x')).toEqual(true); + expect(classList.contains('spinner')).toEqual(true); }); it('should add a special class and a size class', () => { diff --git a/spec/javascripts/vue_shared/components/header_ci_component_spec.js b/spec/javascripts/vue_shared/components/header_ci_component_spec.js index 7a741bdc067..a9c1a67b39b 100644 --- a/spec/javascripts/vue_shared/components/header_ci_component_spec.js +++ b/spec/javascripts/vue_shared/components/header_ci_component_spec.js @@ -88,7 +88,7 @@ describe('Header CI Component', () => { vm.actions[0].isLoading = true; Vue.nextTick(() => { - expect(vm.$el.querySelector('.btn .fa-spinner').getAttribute('style')).toBeFalsy(); + expect(vm.$el.querySelector('.btn .spinner').getAttribute('style')).toBeFalsy(); done(); }); }); diff --git a/spec/lib/backup/uploads_spec.rb b/spec/lib/backup/uploads_spec.rb new file mode 100644 index 00000000000..544d3754c0f --- /dev/null +++ b/spec/lib/backup/uploads_spec.rb @@ -0,0 +1,18 @@ +require 'spec_helper' + +describe Backup::Uploads do + let(:progress) { StringIO.new } + subject(:backup) { described_class.new(progress) } + + describe '#initialize' do + it 'uses the correct upload dir' do + Dir.mktmpdir do |tmpdir| + FileUtils.mkdir_p("#{tmpdir}/uploads") + + allow(Gitlab.config.uploads).to receive(:storage_path) { tmpdir } + + expect(backup.app_files_dir).to eq("#{tmpdir}/uploads") + end + end + end +end diff --git a/spec/lib/gitlab/authorized_keys_spec.rb b/spec/lib/gitlab/authorized_keys_spec.rb new file mode 100644 index 00000000000..b0fbe959ff8 --- /dev/null +++ b/spec/lib/gitlab/authorized_keys_spec.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::AuthorizedKeys do + let(:logger) { double('logger').as_null_object } + + subject { described_class.new(logger) } + + describe '#add_key' do + it "adds a line at the end of the file and strips trailing garbage" do + create_authorized_keys_fixture + auth_line = "command=\"#{Gitlab.config.gitlab_shell.path}/bin/gitlab-shell key-741\",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-rsa AAAAB3NzaDAxx2E" + + expect(logger).to receive(:info).with('Adding key (key-741): ssh-rsa AAAAB3NzaDAxx2E') + expect(subject.add_key('key-741', 'ssh-rsa AAAAB3NzaDAxx2E trailing garbage')) + .to be_truthy + expect(File.read(tmp_authorized_keys_path)).to eq("existing content\n#{auth_line}\n") + end + end + + describe '#batch_add_keys' do + let(:keys) do + [ + double(shell_id: 'key-12', key: 'ssh-dsa ASDFASGADG trailing garbage'), + double(shell_id: 'key-123', key: 'ssh-rsa GFDGDFSGSDFG') + ] + end + + before do + create_authorized_keys_fixture + end + + it "adds lines at the end of the file" do + auth_line1 = "command=\"#{Gitlab.config.gitlab_shell.path}/bin/gitlab-shell key-12\",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-dsa ASDFASGADG" + auth_line2 = "command=\"#{Gitlab.config.gitlab_shell.path}/bin/gitlab-shell key-123\",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-rsa GFDGDFSGSDFG" + + expect(logger).to receive(:info).with('Adding key (key-12): ssh-dsa ASDFASGADG') + expect(logger).to receive(:info).with('Adding key (key-123): ssh-rsa GFDGDFSGSDFG') + expect(subject.batch_add_keys(keys)).to be_truthy + expect(File.read(tmp_authorized_keys_path)).to eq("existing content\n#{auth_line1}\n#{auth_line2}\n") + end + + context "invalid key" do + let(:keys) { [double(shell_id: 'key-123', key: "ssh-rsa A\tSDFA\nSGADG")] } + + it "doesn't add keys" do + expect(subject.batch_add_keys(keys)).to be_falsey + expect(File.read(tmp_authorized_keys_path)).to eq("existing content\n") + end + end + end + + describe '#rm_key' do + it "removes the right line" do + create_authorized_keys_fixture + other_line = "command=\"#{Gitlab.config.gitlab_shell.path}/bin/gitlab-shell key-742\",options ssh-rsa AAAAB3NzaDAxx2E" + delete_line = "command=\"#{Gitlab.config.gitlab_shell.path}/bin/gitlab-shell key-741\",options ssh-rsa AAAAB3NzaDAxx2E" + erased_line = delete_line.gsub(/./, '#') + File.open(tmp_authorized_keys_path, 'a') do |auth_file| + auth_file.puts delete_line + auth_file.puts other_line + end + + expect(logger).to receive(:info).with('Removing key (key-741)') + expect(subject.rm_key('key-741')).to be_truthy + expect(File.read(tmp_authorized_keys_path)).to eq("existing content\n#{erased_line}\n#{other_line}\n") + end + end + + describe '#clear' do + it "should return true" do + expect(subject.clear).to be_truthy + end + end + + describe '#list_key_ids' do + before do + create_authorized_keys_fixture( + existing_content: + "key-1\tssh-dsa AAA\nkey-2\tssh-rsa BBB\nkey-3\tssh-rsa CCC\nkey-9000\tssh-rsa DDD\n" + ) + end + + it 'returns array of key IDs' do + expect(subject.list_key_ids).to eq([1, 2, 3, 9000]) + end + end + + def create_authorized_keys_fixture(existing_content: 'existing content') + FileUtils.mkdir_p(File.dirname(tmp_authorized_keys_path)) + File.open(tmp_authorized_keys_path, 'w') { |file| file.puts(existing_content) } + end + + def tmp_authorized_keys_path + Gitlab.config.gitlab_shell.authorized_keys_file + end +end diff --git a/spec/lib/gitlab/diff/suggestion_diff_spec.rb b/spec/lib/gitlab/diff/suggestion_diff_spec.rb new file mode 100644 index 00000000000..5a32c2bea37 --- /dev/null +++ b/spec/lib/gitlab/diff/suggestion_diff_spec.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Diff::SuggestionDiff do + describe '#diff_lines' do + let(:from_content) do + <<-BLOB.strip_heredoc + "tags": ["devel", "development", "nightly"], + "desktop-file-name-prefix": "(Development) ", + "finish-args": "foo", + BLOB + end + + let(:to_content) do + <<-BLOB.strip_heredoc + "buildsystem": "meson", + "builddir": true, + "name": "nautilus", + "bar": "bar", + BLOB + end + + let(:suggestion) do + instance_double(Suggestion, from_line: 12, + from_content: from_content, + to_content: to_content) + end + + subject { described_class.new(suggestion).diff_lines } + + let(:expected_diff_lines) do + [ + { old_pos: 12, new_pos: 12, type: "match", text: "@@ -12 +12" }, + { old_pos: 12, new_pos: 12, type: "old", text: "-\"tags\": [\"devel\", \"development\", \"nightly\"]," }, + { old_pos: 13, new_pos: 12, type: "old", text: "-\"desktop-file-name-prefix\": \"(Development) \"," }, + { old_pos: 14, new_pos: 12, type: "old", text: "-\"finish-args\": \"foo\"," }, + { old_pos: 15, new_pos: 12, type: "new", text: "+\"buildsystem\": \"meson\"," }, + { old_pos: 15, new_pos: 13, type: "new", text: "+\"builddir\": true," }, + { old_pos: 15, new_pos: 14, type: "new", text: "+\"name\": \"nautilus\"," }, + { old_pos: 15, new_pos: 15, type: "new", text: "+\"bar\": \"bar\"," } + ] + end + + it 'returns diff lines with correct line numbers' do + diff_lines = subject + + expect(diff_lines).to all(be_a(Gitlab::Diff::Line)) + + expected_diff_lines.each_with_index do |expected_line, index| + expect(diff_lines[index].to_hash).to include(expected_line) + end + end + end +end diff --git a/spec/lib/gitlab/fake_application_settings_spec.rb b/spec/lib/gitlab/fake_application_settings_spec.rb index af12e13d36d..c81cb83d9f4 100644 --- a/spec/lib/gitlab/fake_application_settings_spec.rb +++ b/spec/lib/gitlab/fake_application_settings_spec.rb @@ -1,32 +1,33 @@ require 'spec_helper' describe Gitlab::FakeApplicationSettings do - let(:defaults) { { password_authentication_enabled_for_web: false, foobar: 'asdf', signup_enabled: true, 'test?' => 123 } } + let(:defaults) do + described_class.defaults.merge( + foobar: 'asdf', + 'test?' => 123 + ) + end - subject { described_class.new(defaults) } + let(:setting) { described_class.new(defaults) } it 'wraps OpenStruct variables properly' do - expect(subject.password_authentication_enabled_for_web).to be_falsey - expect(subject.signup_enabled).to be_truthy - expect(subject.foobar).to eq('asdf') + expect(setting.password_authentication_enabled_for_web).to be_truthy + expect(setting.signup_enabled).to be_truthy + expect(setting.foobar).to eq('asdf') end it 'defines predicate methods' do - expect(subject.password_authentication_enabled_for_web?).to be_falsey - expect(subject.signup_enabled?).to be_truthy - end - - it 'predicate method changes when value is updated' do - subject.password_authentication_enabled_for_web = true - - expect(subject.password_authentication_enabled_for_web?).to be_truthy + expect(setting.password_authentication_enabled_for_web?).to be_truthy + expect(setting.signup_enabled?).to be_truthy end it 'does not define a predicate method' do - expect(subject.foobar?).to be_nil + expect(setting.foobar?).to be_nil end it 'does not override an existing predicate method' do - expect(subject.test?).to eq(123) + expect(setting.test?).to eq(123) end + + it_behaves_like 'application settings examples' end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 7e6dfa30e37..8ba6862392c 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -1688,6 +1688,11 @@ describe Gitlab::Git::Repository, :seed_helper do expect(repository.delete_config(*%w[does.not.exist test.foo1 test.foo2])).to be_nil + # Workaround for https://github.com/libgit2/rugged/issues/785: If + # Gitaly changes .gitconfig while Rugged has the file loaded + # Rugged::Repository#each_key will report stale values unless a + # lookup is done first. + expect(repository_rugged.config['test.foo1']).to be_nil config_keys = repository_rugged.config.each_key.to_a expect(config_keys).not_to include('test.foo1') expect(config_keys).not_to include('test.foo2') diff --git a/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb b/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb index 15e59718dce..37c3fae7cb7 100644 --- a/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb @@ -273,6 +273,11 @@ describe Gitlab::GithubImport::Importer::PullRequestImporter, :clean_gitlab_redi mr.state = 'opened' mr.save + # Ensure the project owner is creating the branches because the + # merge request author may not have access to push to this + # repository. + allow(project.repository).to receive(:add_branch).with(project.owner, anything, anything).and_call_original + importer.insert_git_data(mr, exists) expect(project.repository.branch_exists?(mr.source_branch)).to be_truthy diff --git a/spec/lib/gitlab/group_search_results_spec.rb b/spec/lib/gitlab/group_search_results_spec.rb new file mode 100644 index 00000000000..2734fcef0a0 --- /dev/null +++ b/spec/lib/gitlab/group_search_results_spec.rb @@ -0,0 +1,69 @@ +require 'spec_helper' + +describe Gitlab::GroupSearchResults do + let(:user) { create(:user) } + + describe 'user search' do + let(:group) { create(:group) } + + it 'returns the users belonging to the group matching the search query' do + user1 = create(:user, username: 'gob_bluth') + create(:group_member, :developer, user: user1, group: group) + + user2 = create(:user, username: 'michael_bluth') + create(:group_member, :developer, user: user2, group: group) + + create(:user, username: 'gob_2018') + + result = described_class.new(user, anything, group, 'gob').objects('users') + + expect(result).to eq [user1] + end + + it 'returns the user belonging to the subgroup matching the search query', :nested_groups do + user1 = create(:user, username: 'gob_bluth') + subgroup = create(:group, parent: group) + create(:group_member, :developer, user: user1, group: subgroup) + + create(:user, username: 'gob_2018') + + result = described_class.new(user, anything, group, 'gob').objects('users') + + expect(result).to eq [user1] + end + + it 'returns the user belonging to the parent group matching the search query', :nested_groups do + user1 = create(:user, username: 'gob_bluth') + parent_group = create(:group, children: [group]) + create(:group_member, :developer, user: user1, group: parent_group) + + create(:user, username: 'gob_2018') + + result = described_class.new(user, anything, group, 'gob').objects('users') + + expect(result).to eq [user1] + end + + it 'does not return the user belonging to the private subgroup', :nested_groups do + user1 = create(:user, username: 'gob_bluth') + subgroup = create(:group, :private, parent: group) + create(:group_member, :developer, user: user1, group: subgroup) + + create(:user, username: 'gob_2018') + + result = described_class.new(user, anything, group, 'gob').objects('users') + + expect(result).to eq [] + end + + it 'does not return the user belonging to an unrelated group' do + user = create(:user, username: 'gob_bluth') + unrelated_group = create(:group) + create(:group_member, :developer, user: user, group: unrelated_group) + + result = described_class.new(user, anything, group, 'gob').objects('users') + + expect(result).to eq [] + end + end +end diff --git a/spec/lib/gitlab/hashed_storage/migrator_spec.rb b/spec/lib/gitlab/hashed_storage/migrator_spec.rb index d03a74ac9eb..8e253b51597 100644 --- a/spec/lib/gitlab/hashed_storage/migrator_spec.rb +++ b/spec/lib/gitlab/hashed_storage/migrator_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe Gitlab::HashedStorage::Migrator, :sidekiq do +describe Gitlab::HashedStorage::Migrator, :sidekiq, :redis do describe '#bulk_schedule_migration' do it 'schedules job to HashedStorage::MigratorWorker' do Sidekiq::Testing.fake! do @@ -189,7 +189,7 @@ describe Gitlab::HashedStorage::Migrator, :sidekiq do set(:project) { create(:project, :empty_repo) } it 'returns true when there are MigratorWorker jobs scheduled' do - Sidekiq::Testing.fake! do + Sidekiq::Testing.disable! do ::HashedStorage::MigratorWorker.perform_async(1, 5) expect(subject.migration_pending?).to be_truthy @@ -197,7 +197,7 @@ describe Gitlab::HashedStorage::Migrator, :sidekiq do end it 'returns true when there are ProjectMigrateWorker jobs scheduled' do - Sidekiq::Testing.fake! do + Sidekiq::Testing.disable! do ::HashedStorage::ProjectMigrateWorker.perform_async(1) expect(subject.migration_pending?).to be_truthy @@ -213,7 +213,7 @@ describe Gitlab::HashedStorage::Migrator, :sidekiq do set(:project) { create(:project, :empty_repo) } it 'returns true when there are RollbackerWorker jobs scheduled' do - Sidekiq::Testing.fake! do + Sidekiq::Testing.disable! do ::HashedStorage::RollbackerWorker.perform_async(1, 5) expect(subject.rollback_pending?).to be_truthy @@ -221,7 +221,7 @@ describe Gitlab::HashedStorage::Migrator, :sidekiq do end it 'returns true when there are jobs scheduled' do - Sidekiq::Testing.fake! do + Sidekiq::Testing.disable! do ::HashedStorage::ProjectRollbackWorker.perform_async(1) expect(subject.rollback_pending?).to be_truthy diff --git a/spec/lib/gitlab/json_cache_spec.rb b/spec/lib/gitlab/json_cache_spec.rb index 2cae8ec031a..b7dc8234bdf 100644 --- a/spec/lib/gitlab/json_cache_spec.rb +++ b/spec/lib/gitlab/json_cache_spec.rb @@ -7,7 +7,7 @@ describe Gitlab::JsonCache do let(:namespace) { 'geo' } let(:key) { 'foo' } let(:expanded_key) { "#{namespace}:#{key}:#{Rails.version}" } - let(:broadcast_message) { create(:broadcast_message) } + set(:broadcast_message) { create(:broadcast_message) } subject(:cache) { described_class.new(namespace: namespace, backend: backend) } @@ -321,6 +321,42 @@ describe Gitlab::JsonCache do expect(result).to be_new_record end + + it 'gracefully handles bad cached entry' do + allow(backend).to receive(:read) + .with(expanded_key) + .and_return('{') + + expect(cache.read(key, BroadcastMessage)).to be_nil + end + + it 'gracefully handles an empty hash' do + allow(backend).to receive(:read) + .with(expanded_key) + .and_return('{}') + + expect(cache.read(key, BroadcastMessage)).to be_a(BroadcastMessage) + end + + it 'gracefully handles unknown attributes' do + allow(backend).to receive(:read) + .with(expanded_key) + .and_return(broadcast_message.attributes.merge(unknown_attribute: 1).to_json) + + expect(cache.read(key, BroadcastMessage)).to be_nil + end + + it 'gracefully handles excluded fields from attributes during serialization' do + backend.write(expanded_key, broadcast_message.to_json) + + result = cache.fetch(key, as: BroadcastMessage) { 'block result' } + + excluded_fields = BroadcastMessage.cached_markdown_fields.html_fields + + (excluded_fields + ['cached_markdown_version']).each do |field| + expect(result.public_send(field)).to be_nil + end + end end it "returns the result of the block when 'as' option is nil" do diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb index 6831274d37c..4a41d5cf51e 100644 --- a/spec/lib/gitlab/project_search_results_spec.rb +++ b/spec/lib/gitlab/project_search_results_spec.rb @@ -412,4 +412,36 @@ describe Gitlab::ProjectSearchResults do end end end + + describe 'user search' do + it 'returns the user belonging to the project matching the search query' do + project = create(:project) + + user1 = create(:user, username: 'gob_bluth') + create(:project_member, :developer, user: user1, project: project) + + user2 = create(:user, username: 'michael_bluth') + create(:project_member, :developer, user: user2, project: project) + + create(:user, username: 'gob_2018') + + result = described_class.new(user, project, 'gob').objects('users') + + expect(result).to eq [user1] + end + + it 'returns the user belonging to the group matching the search query' do + group = create(:group) + project = create(:project, namespace: group) + + user1 = create(:user, username: 'gob_bluth') + create(:group_member, :developer, user: user1, group: group) + + create(:user, username: 'gob_2018') + + result = described_class.new(user, project, 'gob').objects('users') + + expect(result).to eq [user1] + end + end end diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb index 87288baedb0..4b57eecff93 100644 --- a/spec/lib/gitlab/search_results_spec.rb +++ b/spec/lib/gitlab/search_results_spec.rb @@ -121,6 +121,22 @@ describe Gitlab::SearchResults do results.objects('issues') end end + + describe '#users' do + it 'does not call the UsersFinder when the current_user is not allowed to read users list' do + allow(Ability).to receive(:allowed?).and_return(false) + + expect(UsersFinder).not_to receive(:new).with(user, search: 'foo').and_call_original + + results.objects('users') + end + + it 'calls the UsersFinder' do + expect(UsersFinder).to receive(:new).with(user, search: 'foo').and_call_original + + results.objects('users') + end + end end it 'does not list issues on private projects' do diff --git a/spec/lib/gitlab/shell_spec.rb b/spec/lib/gitlab/shell_spec.rb index d6aadf0f7de..e2f09de2808 100644 --- a/spec/lib/gitlab/shell_spec.rb +++ b/spec/lib/gitlab/shell_spec.rb @@ -8,6 +8,7 @@ describe Gitlab::Shell do let(:gitlab_shell) { described_class.new } let(:popen_vars) { { 'GIT_TERMINAL_PROMPT' => ENV['GIT_TERMINAL_PROMPT'] } } let(:timeout) { Gitlab.config.gitlab_shell.git_timeout } + let(:gitlab_authorized_keys) { double } before do allow(Project).to receive(:find).and_return(project) @@ -49,13 +50,38 @@ describe Gitlab::Shell do describe '#add_key' do context 'when authorized_keys_enabled is true' do - it 'removes trailing garbage' do - allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path) - expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with( - [:gitlab_shell_keys_path, 'add-key', 'key-123', 'ssh-rsa foobar'] - ) + context 'authorized_keys_file not set' do + before do + stub_gitlab_shell_setting(authorized_keys_file: nil) + allow(gitlab_shell) + .to receive(:gitlab_shell_keys_path) + .and_return(:gitlab_shell_keys_path) + end + + it 'calls #gitlab_shell_fast_execute with add-key command' do + expect(gitlab_shell) + .to receive(:gitlab_shell_fast_execute) + .with([ + :gitlab_shell_keys_path, + 'add-key', + 'key-123', + 'ssh-rsa foobar' + ]) + + gitlab_shell.add_key('key-123', 'ssh-rsa foobar trailing garbage') + end + end + + context 'authorized_keys_file set' do + it 'calls Gitlab::AuthorizedKeys#add_key with id and key' do + expect(Gitlab::AuthorizedKeys).to receive(:new).and_return(gitlab_authorized_keys) - gitlab_shell.add_key('key-123', 'ssh-rsa foobar trailing garbage') + expect(gitlab_authorized_keys) + .to receive(:add_key) + .with('key-123', 'ssh-rsa foobar') + + gitlab_shell.add_key('key-123', 'ssh-rsa foobar') + end end end @@ -64,10 +90,24 @@ describe Gitlab::Shell do stub_application_setting(authorized_keys_enabled: false) end - it 'does nothing' do - expect(gitlab_shell).not_to receive(:gitlab_shell_fast_execute) + context 'authorized_keys_file not set' do + before do + stub_gitlab_shell_setting(authorized_keys_file: nil) + end + + it 'does nothing' do + expect(gitlab_shell).not_to receive(:gitlab_shell_fast_execute) + + gitlab_shell.add_key('key-123', 'ssh-rsa foobar trailing garbage') + end + end + + context 'authorized_keys_file set' do + it 'does nothing' do + expect(Gitlab::AuthorizedKeys).not_to receive(:new) - gitlab_shell.add_key('key-123', 'ssh-rsa foobar trailing garbage') + gitlab_shell.add_key('key-123', 'ssh-rsa foobar trailing garbage') + end end end @@ -76,24 +116,89 @@ describe Gitlab::Shell do stub_application_setting(authorized_keys_enabled: nil) end - it 'removes trailing garbage' do - allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path) - expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with( - [:gitlab_shell_keys_path, 'add-key', 'key-123', 'ssh-rsa foobar'] - ) + context 'authorized_keys_file not set' do + before do + stub_gitlab_shell_setting(authorized_keys_file: nil) + allow(gitlab_shell) + .to receive(:gitlab_shell_keys_path) + .and_return(:gitlab_shell_keys_path) + end + + it 'calls #gitlab_shell_fast_execute with add-key command' do + expect(gitlab_shell) + .to receive(:gitlab_shell_fast_execute) + .with([ + :gitlab_shell_keys_path, + 'add-key', + 'key-123', + 'ssh-rsa foobar' + ]) + + gitlab_shell.add_key('key-123', 'ssh-rsa foobar trailing garbage') + end + end + + context 'authorized_keys_file set' do + it 'calls Gitlab::AuthorizedKeys#add_key with id and key' do + expect(Gitlab::AuthorizedKeys).to receive(:new).and_return(gitlab_authorized_keys) + + expect(gitlab_authorized_keys) + .to receive(:add_key) + .with('key-123', 'ssh-rsa foobar') - gitlab_shell.add_key('key-123', 'ssh-rsa foobar trailing garbage') + gitlab_shell.add_key('key-123', 'ssh-rsa foobar') + end end end end describe '#batch_add_keys' do + let(:keys) { [double(shell_id: 'key-123', key: 'ssh-rsa foobar')] } + context 'when authorized_keys_enabled is true' do - it 'instantiates KeyAdder' do - expect_any_instance_of(Gitlab::Shell::KeyAdder).to receive(:add_key).with('key-123', 'ssh-rsa foobar') + context 'authorized_keys_file not set' do + let(:io) { double } + + before do + stub_gitlab_shell_setting(authorized_keys_file: nil) + end + + context 'valid keys' do + before do + allow(gitlab_shell) + .to receive(:gitlab_shell_keys_path) + .and_return(:gitlab_shell_keys_path) + end + + it 'calls gitlab-keys with batch-add-keys command' do + expect(IO) + .to receive(:popen) + .with("gitlab_shell_keys_path batch-add-keys", 'w') + .and_yield(io) + + expect(io).to receive(:puts).with("key-123\tssh-rsa foobar") + expect(gitlab_shell.batch_add_keys(keys)).to be_truthy + end + end + + context 'invalid keys' do + let(:keys) { [double(shell_id: 'key-123', key: "ssh-rsa A\tSDFA\nSGADG")] } + + it 'catches failure and returns false' do + expect(gitlab_shell.batch_add_keys(keys)).to be_falsey + end + end + end - gitlab_shell.batch_add_keys do |adder| - adder.add_key('key-123', 'ssh-rsa foobar') + context 'authorized_keys_file set' do + it 'calls Gitlab::AuthorizedKeys#batch_add_keys with keys to be added' do + expect(Gitlab::AuthorizedKeys).to receive(:new).and_return(gitlab_authorized_keys) + + expect(gitlab_authorized_keys) + .to receive(:batch_add_keys) + .with(keys) + + gitlab_shell.batch_add_keys(keys) end end end @@ -103,11 +208,23 @@ describe Gitlab::Shell do stub_application_setting(authorized_keys_enabled: false) end - it 'does nothing' do - expect_any_instance_of(Gitlab::Shell::KeyAdder).not_to receive(:add_key) + context 'authorized_keys_file not set' do + before do + stub_gitlab_shell_setting(authorized_keys_file: nil) + end + + it 'does nothing' do + expect(IO).not_to receive(:popen) + + gitlab_shell.batch_add_keys(keys) + end + end + + context 'authorized_keys_file set' do + it 'does nothing' do + expect(Gitlab::AuthorizedKeys).not_to receive(:new) - gitlab_shell.batch_add_keys do |adder| - adder.add_key('key-123', 'ssh-rsa foobar') + gitlab_shell.batch_add_keys(keys) end end end @@ -117,11 +234,37 @@ describe Gitlab::Shell do stub_application_setting(authorized_keys_enabled: nil) end - it 'instantiates KeyAdder' do - expect_any_instance_of(Gitlab::Shell::KeyAdder).to receive(:add_key).with('key-123', 'ssh-rsa foobar') + context 'authorized_keys_file not set' do + let(:io) { double } - gitlab_shell.batch_add_keys do |adder| - adder.add_key('key-123', 'ssh-rsa foobar') + before do + stub_gitlab_shell_setting(authorized_keys_file: nil) + allow(gitlab_shell) + .to receive(:gitlab_shell_keys_path) + .and_return(:gitlab_shell_keys_path) + end + + it 'calls gitlab-keys with batch-add-keys command' do + expect(IO) + .to receive(:popen) + .with("gitlab_shell_keys_path batch-add-keys", 'w') + .and_yield(io) + + expect(io).to receive(:puts).with("key-123\tssh-rsa foobar") + + gitlab_shell.batch_add_keys(keys) + end + end + + context 'authorized_keys_file set' do + it 'calls Gitlab::AuthorizedKeys#batch_add_keys with keys to be added' do + expect(Gitlab::AuthorizedKeys).to receive(:new).and_return(gitlab_authorized_keys) + + expect(gitlab_authorized_keys) + .to receive(:batch_add_keys) + .with(keys) + + gitlab_shell.batch_add_keys(keys) end end end @@ -129,13 +272,34 @@ describe Gitlab::Shell do describe '#remove_key' do context 'when authorized_keys_enabled is true' do - it 'removes trailing garbage' do - allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path) - expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with( - [:gitlab_shell_keys_path, 'rm-key', 'key-123', 'ssh-rsa foobar'] - ) + context 'authorized_keys_file not set' do + before do + stub_gitlab_shell_setting(authorized_keys_file: nil) + allow(gitlab_shell) + .to receive(:gitlab_shell_keys_path) + .and_return(:gitlab_shell_keys_path) + end - gitlab_shell.remove_key('key-123', 'ssh-rsa foobar') + it 'calls #gitlab_shell_fast_execute with rm-key command' do + expect(gitlab_shell) + .to receive(:gitlab_shell_fast_execute) + .with([ + :gitlab_shell_keys_path, + 'rm-key', + 'key-123' + ]) + + gitlab_shell.remove_key('key-123') + end + end + + context 'authorized_keys_file not set' do + it 'calls Gitlab::AuthorizedKeys#rm_key with the key to be removed' do + expect(Gitlab::AuthorizedKeys).to receive(:new).and_return(gitlab_authorized_keys) + expect(gitlab_authorized_keys).to receive(:rm_key).with('key-123') + + gitlab_shell.remove_key('key-123') + end end end @@ -144,10 +308,24 @@ describe Gitlab::Shell do stub_application_setting(authorized_keys_enabled: false) end - it 'does nothing' do - expect(gitlab_shell).not_to receive(:gitlab_shell_fast_execute) + context 'authorized_keys_file not set' do + before do + stub_gitlab_shell_setting(authorized_keys_file: nil) + end + + it 'does nothing' do + expect(gitlab_shell).not_to receive(:gitlab_shell_fast_execute) - gitlab_shell.remove_key('key-123', 'ssh-rsa foobar') + gitlab_shell.remove_key('key-123') + end + end + + context 'authorized_keys_file set' do + it 'does nothing' do + expect(Gitlab::AuthorizedKeys).not_to receive(:new) + + gitlab_shell.remove_key('key-123') + end end end @@ -156,232 +334,256 @@ describe Gitlab::Shell do stub_application_setting(authorized_keys_enabled: nil) end - it 'removes trailing garbage' do - allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path) - expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with( - [:gitlab_shell_keys_path, 'rm-key', 'key-123', 'ssh-rsa foobar'] - ) + context 'authorized_keys_file not set' do + before do + stub_gitlab_shell_setting(authorized_keys_file: nil) + allow(gitlab_shell) + .to receive(:gitlab_shell_keys_path) + .and_return(:gitlab_shell_keys_path) + end + + it 'calls #gitlab_shell_fast_execute with rm-key command' do + expect(gitlab_shell) + .to receive(:gitlab_shell_fast_execute) + .with([ + :gitlab_shell_keys_path, + 'rm-key', + 'key-123' + ]) - gitlab_shell.remove_key('key-123', 'ssh-rsa foobar') + gitlab_shell.remove_key('key-123') + end end - end - context 'when key content is not given' do - it 'calls rm-key with only one argument' do - allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path) - expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with( - [:gitlab_shell_keys_path, 'rm-key', 'key-123'] - ) + context 'authorized_keys_file not set' do + it 'calls Gitlab::AuthorizedKeys#rm_key with the key to be removed' do + expect(Gitlab::AuthorizedKeys).to receive(:new).and_return(gitlab_authorized_keys) + expect(gitlab_authorized_keys).to receive(:rm_key).with('key-123') - gitlab_shell.remove_key('key-123') + gitlab_shell.remove_key('key-123') + end end end end describe '#remove_all_keys' do context 'when authorized_keys_enabled is true' do - it 'removes trailing garbage' do - allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path) - expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with([:gitlab_shell_keys_path, 'clear']) + context 'authorized_keys_file not set' do + before do + stub_gitlab_shell_setting(authorized_keys_file: nil) + allow(gitlab_shell) + .to receive(:gitlab_shell_keys_path) + .and_return(:gitlab_shell_keys_path) + end - gitlab_shell.remove_all_keys - end - end + it 'calls #gitlab_shell_fast_execute with clear command' do + expect(gitlab_shell) + .to receive(:gitlab_shell_fast_execute) + .with([:gitlab_shell_keys_path, 'clear']) - context 'when authorized_keys_enabled is false' do - before do - stub_application_setting(authorized_keys_enabled: false) + gitlab_shell.remove_all_keys + end end - it 'does nothing' do - expect(gitlab_shell).not_to receive(:gitlab_shell_fast_execute) + context 'authorized_keys_file set' do + it 'calls Gitlab::AuthorizedKeys#clear' do + expect(Gitlab::AuthorizedKeys).to receive(:new).and_return(gitlab_authorized_keys) + expect(gitlab_authorized_keys).to receive(:clear) - gitlab_shell.remove_all_keys + gitlab_shell.remove_all_keys + end end end - context 'when authorized_keys_enabled is nil' do + context 'when authorized_keys_enabled is false' do before do - stub_application_setting(authorized_keys_enabled: nil) + stub_application_setting(authorized_keys_enabled: false) end - it 'removes trailing garbage' do - allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path) - expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with( - [:gitlab_shell_keys_path, 'clear'] - ) + context 'authorized_keys_file not set' do + before do + stub_gitlab_shell_setting(authorized_keys_file: nil) + end - gitlab_shell.remove_all_keys - end - end - end + it 'does nothing' do + expect(gitlab_shell).not_to receive(:gitlab_shell_fast_execute) - describe '#remove_keys_not_found_in_db' do - context 'when keys are in the file that are not in the DB' do - before do - gitlab_shell.remove_all_keys - gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF') - gitlab_shell.add_key('key-9876', 'ssh-rsa ASDFASDF') - @another_key = create(:key) # this one IS in the DB + gitlab_shell.remove_all_keys + end end - it 'removes the keys' do - expect(find_in_authorized_keys_file(1234)).to be_truthy - expect(find_in_authorized_keys_file(9876)).to be_truthy - expect(find_in_authorized_keys_file(@another_key.id)).to be_truthy - gitlab_shell.remove_keys_not_found_in_db - expect(find_in_authorized_keys_file(1234)).to be_falsey - expect(find_in_authorized_keys_file(9876)).to be_falsey - expect(find_in_authorized_keys_file(@another_key.id)).to be_truthy + context 'authorized_keys_file set' do + it 'does nothing' do + expect(Gitlab::AuthorizedKeys).not_to receive(:new) + + gitlab_shell.remove_all_keys + end end end - context 'when keys there are duplicate keys in the file that are not in the DB' do + context 'when authorized_keys_enabled is nil' do before do - gitlab_shell.remove_all_keys - gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF') - gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF') + stub_application_setting(authorized_keys_enabled: nil) end - it 'removes the keys' do - expect(find_in_authorized_keys_file(1234)).to be_truthy - gitlab_shell.remove_keys_not_found_in_db - expect(find_in_authorized_keys_file(1234)).to be_falsey - end + context 'authorized_keys_file not set' do + before do + stub_gitlab_shell_setting(authorized_keys_file: nil) + allow(gitlab_shell) + .to receive(:gitlab_shell_keys_path) + .and_return(:gitlab_shell_keys_path) + end - it 'does not run remove more than once per key (in a batch)' do - expect(gitlab_shell).to receive(:remove_key).with('key-1234').once - gitlab_shell.remove_keys_not_found_in_db - end - end + it 'calls #gitlab_shell_fast_execute with clear command' do + expect(gitlab_shell) + .to receive(:gitlab_shell_fast_execute) + .with([:gitlab_shell_keys_path, 'clear']) - context 'when keys there are duplicate keys in the file that ARE in the DB' do - before do - gitlab_shell.remove_all_keys - @key = create(:key) - gitlab_shell.add_key(@key.shell_id, @key.key) + gitlab_shell.remove_all_keys + end end - it 'does not remove the key' do - gitlab_shell.remove_keys_not_found_in_db - expect(find_in_authorized_keys_file(@key.id)).to be_truthy - end + context 'authorized_keys_file set' do + it 'calls Gitlab::AuthorizedKeys#clear' do + expect(Gitlab::AuthorizedKeys).to receive(:new).and_return(gitlab_authorized_keys) + expect(gitlab_authorized_keys).to receive(:clear) - it 'does not need to run a SELECT query for that batch, on account of that key' do - expect_any_instance_of(ActiveRecord::Relation).not_to receive(:pluck) - gitlab_shell.remove_keys_not_found_in_db + gitlab_shell.remove_all_keys + end end end + end - unless ENV['CI'] # Skip in CI, it takes 1 minute - context 'when the first batch can be skipped, but the next batch has keys that are not in the DB' do + describe '#remove_keys_not_found_in_db' do + context 'when keys are in the file that are not in the DB' do + context 'authorized_keys_file not set' do before do + stub_gitlab_shell_setting(authorized_keys_file: nil) gitlab_shell.remove_all_keys - 100.times { |i| create(:key) } # first batch is all in the DB gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF') + gitlab_shell.add_key('key-9876', 'ssh-rsa ASDFASDF') + @another_key = create(:key) # this one IS in the DB end - it 'removes the keys not in the DB' do - expect(find_in_authorized_keys_file(1234)).to be_truthy + it 'removes the keys' do + expect(gitlab_shell).to receive(:remove_key).with('key-1234') + expect(gitlab_shell).to receive(:remove_key).with('key-9876') + expect(gitlab_shell).not_to receive(:remove_key).with("key-#{@another_key.id}") + gitlab_shell.remove_keys_not_found_in_db - expect(find_in_authorized_keys_file(1234)).to be_falsey end end - end - end - describe '#batch_read_key_ids' do - context 'when there are keys in the authorized_keys file' do - before do - gitlab_shell.remove_all_keys - (1..4).each do |i| - gitlab_shell.add_key("key-#{i}", "ssh-rsa ASDFASDF#{i}") + context 'authorized_keys_file set' do + before do + gitlab_shell.remove_all_keys + gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF') + gitlab_shell.add_key('key-9876', 'ssh-rsa ASDFASDF') + @another_key = create(:key) # this one IS in the DB end - end - it 'iterates over the key IDs in the file, in batches' do - loop_count = 0 - first_batch = [1, 2] - second_batch = [3, 4] + it 'removes the keys' do + expect(gitlab_shell).to receive(:remove_key).with('key-1234') + expect(gitlab_shell).to receive(:remove_key).with('key-9876') + expect(gitlab_shell).not_to receive(:remove_key).with("key-#{@another_key.id}") - gitlab_shell.batch_read_key_ids(batch_size: 2) do |batch| - expected = (loop_count == 0 ? first_batch : second_batch) - expect(batch).to eq(expected) - loop_count += 1 + gitlab_shell.remove_keys_not_found_in_db end end end - end - describe '#list_key_ids' do - context 'when there are keys in the authorized_keys file' do - before do - gitlab_shell.remove_all_keys - (1..4).each do |i| - gitlab_shell.add_key("key-#{i}", "ssh-rsa ASDFASDF#{i}") + context 'when keys there are duplicate keys in the file that are not in the DB' do + context 'authorized_keys_file not set' do + before do + stub_gitlab_shell_setting(authorized_keys_file: nil) + gitlab_shell.remove_all_keys + gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF') + gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF') + end + + it 'removes the keys' do + expect(gitlab_shell).to receive(:remove_key).with('key-1234') + + gitlab_shell.remove_keys_not_found_in_db end end - it 'outputs the key IDs in the file, separated by newlines' do - ids = [] - gitlab_shell.list_key_ids do |io| - io.each do |line| - ids << line - end + context 'authorized_keys_file set' do + before do + gitlab_shell.remove_all_keys + gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF') + gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF') end - expect(ids).to eq(%W{1\n 2\n 3\n 4\n}) - end - end + it 'removes the keys' do + expect(gitlab_shell).to receive(:remove_key).with('key-1234') - context 'when there are no keys in the authorized_keys file' do - before do - gitlab_shell.remove_all_keys + gitlab_shell.remove_keys_not_found_in_db + end end + end - it 'outputs nothing, not even an empty string' do - ids = [] - gitlab_shell.list_key_ids do |io| - io.each do |line| - ids << line - end + context 'when keys there are duplicate keys in the file that ARE in the DB' do + context 'authorized_keys_file not set' do + before do + stub_gitlab_shell_setting(authorized_keys_file: nil) + gitlab_shell.remove_all_keys + @key = create(:key) + gitlab_shell.add_key(@key.shell_id, @key.key) end - expect(ids).to eq([]) + it 'does not remove the key' do + expect(gitlab_shell).not_to receive(:remove_key).with("key-#{@key.id}") + + gitlab_shell.remove_keys_not_found_in_db + end end - end - end - describe Gitlab::Shell::KeyAdder do - describe '#add_key' do - it 'removes trailing garbage' do - io = spy(:io) - adder = described_class.new(io) + context 'authorized_keys_file set' do + before do + gitlab_shell.remove_all_keys + @key = create(:key) + gitlab_shell.add_key(@key.shell_id, @key.key) + end - adder.add_key('key-42', "ssh-rsa foo bar\tbaz") + it 'does not remove the key' do + expect(gitlab_shell).not_to receive(:remove_key).with("key-#{@key.id}") - expect(io).to have_received(:puts).with("key-42\tssh-rsa foo") + gitlab_shell.remove_keys_not_found_in_db + end end + end - it 'handles multiple spaces in the key' do - io = spy(:io) - adder = described_class.new(io) + unless ENV['CI'] # Skip in CI, it takes 1 minute + context 'when the first batch can be skipped, but the next batch has keys that are not in the DB' do + context 'authorized_keys_file not set' do + before do + stub_gitlab_shell_setting(authorized_keys_file: nil) + gitlab_shell.remove_all_keys + 100.times { |i| create(:key) } # first batch is all in the DB + gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF') + end - adder.add_key('key-42', "ssh-rsa foo") + it 'removes the keys not in the DB' do + expect(gitlab_shell).to receive(:remove_key).with('key-1234') - expect(io).to have_received(:puts).with("key-42\tssh-rsa foo") - end + gitlab_shell.remove_keys_not_found_in_db + end + end - it 'raises an exception if the key contains a tab' do - expect do - described_class.new(StringIO.new).add_key('key-42', "ssh-rsa\tfoobar") - end.to raise_error(Gitlab::Shell::Error) - end + context 'authorized_keys_file set' do + before do + gitlab_shell.remove_all_keys + 100.times { |i| create(:key) } # first batch is all in the DB + gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF') + end + + it 'removes the keys not in the DB' do + expect(gitlab_shell).to receive(:remove_key).with('key-1234') - it 'raises an exception if the key contains a newline' do - expect do - described_class.new(StringIO.new).add_key('key-42', "ssh-rsa foobar\nssh-rsa pawned") - end.to raise_error(Gitlab::Shell::Error) + gitlab_shell.remove_keys_not_found_in_db + end + end end end end @@ -566,12 +768,4 @@ describe Gitlab::Shell do end end end - - def find_in_authorized_keys_file(key_id) - gitlab_shell.batch_read_key_ids do |ids| - return true if ids.include?(key_id) # rubocop:disable Cop/AvoidReturnFromBlocks - end - - false - end end diff --git a/spec/lib/gitlab/user_extractor_spec.rb b/spec/lib/gitlab/user_extractor_spec.rb index 6e2bb81fbda..b86ec5445b8 100644 --- a/spec/lib/gitlab/user_extractor_spec.rb +++ b/spec/lib/gitlab/user_extractor_spec.rb @@ -38,6 +38,18 @@ describe Gitlab::UserExtractor do expect(extractor.users).to include(user) end + + context 'input as array of strings' do + it 'is treated as one string' do + extractor = described_class.new(text.lines) + + user_1 = create(:user, username: "USER-1") + user_4 = create(:user, username: "USER-4") + user_email = create(:user, email: 'user@gitlab.org') + + expect(extractor.users).to contain_exactly(user_1, user_4, user_email) + end + end end describe '#matches' do diff --git a/spec/lib/gitlab/utils_spec.rb b/spec/lib/gitlab/utils_spec.rb index 8f5029b3565..4645339f439 100644 --- a/spec/lib/gitlab/utils_spec.rb +++ b/spec/lib/gitlab/utils_spec.rb @@ -213,4 +213,22 @@ describe Gitlab::Utils do expect(subject[:variables].first[:key]).to eq('VAR1') end end + + describe '.try_megabytes_to_bytes' do + context 'when the size can be converted to megabytes' do + it 'returns the size in megabytes' do + size = described_class.try_megabytes_to_bytes(1) + + expect(size).to eq(1.megabytes) + end + end + + context 'when the size can not be converted to megabytes' do + it 'returns the input size' do + size = described_class.try_megabytes_to_bytes('foo') + + expect(size).to eq('foo') + end + end + end end diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index 789e14e8a20..314f0728b8e 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -117,14 +117,6 @@ describe ApplicationSetting do it { expect(setting.repository_storages).to eq(['default']) } end - context '#commit_email_hostname' do - it 'returns configured gitlab hostname if commit_email_hostname is not defined' do - setting.update(commit_email_hostname: nil) - - expect(setting.commit_email_hostname).to eq("users.noreply.#{Gitlab.config.gitlab.host}") - end - end - context 'auto_devops_domain setting' do context 'when auto_devops_enabled? is true' do before do @@ -182,15 +174,6 @@ describe ApplicationSetting do it { is_expected.not_to allow_value("").for(:repository_storages) } it { is_expected.not_to allow_value(nil).for(:repository_storages) } end - - describe '.pick_repository_storage' do - it 'uses Array#sample to pick a random storage' do - array = double('array', sample: 'random') - expect(setting).to receive(:repository_storages).and_return(array) - - expect(setting.pick_repository_storage).to eq('random') - end - end end context 'housekeeping settings' do @@ -367,65 +350,6 @@ describe ApplicationSetting do end end - context 'restricted signup domains' do - it 'sets single domain' do - setting.domain_whitelist_raw = 'example.com' - expect(setting.domain_whitelist).to eq(['example.com']) - end - - it 'sets multiple domains with spaces' do - setting.domain_whitelist_raw = 'example.com *.example.com' - expect(setting.domain_whitelist).to eq(['example.com', '*.example.com']) - end - - it 'sets multiple domains with newlines and a space' do - setting.domain_whitelist_raw = "example.com\n *.example.com" - expect(setting.domain_whitelist).to eq(['example.com', '*.example.com']) - end - - it 'sets multiple domains with commas' do - setting.domain_whitelist_raw = "example.com, *.example.com" - expect(setting.domain_whitelist).to eq(['example.com', '*.example.com']) - end - end - - context 'blacklisted signup domains' do - it 'sets single domain' do - setting.domain_blacklist_raw = 'example.com' - expect(setting.domain_blacklist).to contain_exactly('example.com') - end - - it 'sets multiple domains with spaces' do - setting.domain_blacklist_raw = 'example.com *.example.com' - expect(setting.domain_blacklist).to contain_exactly('example.com', '*.example.com') - end - - it 'sets multiple domains with newlines and a space' do - setting.domain_blacklist_raw = "example.com\n *.example.com" - expect(setting.domain_blacklist).to contain_exactly('example.com', '*.example.com') - end - - it 'sets multiple domains with commas' do - setting.domain_blacklist_raw = "example.com, *.example.com" - expect(setting.domain_blacklist).to contain_exactly('example.com', '*.example.com') - end - - it 'sets multiple domains with semicolon' do - setting.domain_blacklist_raw = "example.com; *.example.com" - expect(setting.domain_blacklist).to contain_exactly('example.com', '*.example.com') - end - - it 'sets multiple domains with mixture of everything' do - setting.domain_blacklist_raw = "example.com; *.example.com\n test.com\sblock.com yes.com" - expect(setting.domain_blacklist).to contain_exactly('example.com', '*.example.com', 'test.com', 'block.com', 'yes.com') - end - - it 'sets multiple domain with file' do - setting.domain_blacklist_file = File.open(Rails.root.join('spec/fixtures/', 'domain_blacklist.txt')) - expect(setting.domain_blacklist).to contain_exactly('example.com', 'test.com', 'foo.bar') - end - end - describe 'performance bar settings' do describe 'performance_bar_allowed_group' do context 'with no performance_bar_allowed_group_id saved' do @@ -462,142 +386,6 @@ describe ApplicationSetting do end end - describe 'usage ping settings' do - context 'when the usage ping is disabled in gitlab.yml' do - before do - allow(Settings.gitlab).to receive(:usage_ping_enabled).and_return(false) - end - - it 'does not allow the usage ping to be configured' do - expect(setting.usage_ping_can_be_configured?).to be_falsey - end - - context 'when the usage ping is disabled in the DB' do - before do - setting.usage_ping_enabled = false - end - - it 'returns false for usage_ping_enabled' do - expect(setting.usage_ping_enabled).to be_falsey - end - end - - context 'when the usage ping is enabled in the DB' do - before do - setting.usage_ping_enabled = true - end - - it 'returns false for usage_ping_enabled' do - expect(setting.usage_ping_enabled).to be_falsey - end - end - end - - context 'when the usage ping is enabled in gitlab.yml' do - before do - allow(Settings.gitlab).to receive(:usage_ping_enabled).and_return(true) - end - - it 'allows the usage ping to be configured' do - expect(setting.usage_ping_can_be_configured?).to be_truthy - end - - context 'when the usage ping is disabled in the DB' do - before do - setting.usage_ping_enabled = false - end - - it 'returns false for usage_ping_enabled' do - expect(setting.usage_ping_enabled).to be_falsey - end - end - - context 'when the usage ping is enabled in the DB' do - before do - setting.usage_ping_enabled = true - end - - it 'returns true for usage_ping_enabled' do - expect(setting.usage_ping_enabled).to be_truthy - end - end - end - end - - describe '#allowed_key_types' do - it 'includes all key types by default' do - expect(setting.allowed_key_types).to contain_exactly(*described_class::SUPPORTED_KEY_TYPES) - end - - it 'excludes disabled key types' do - expect(setting.allowed_key_types).to include(:ed25519) - - setting.ed25519_key_restriction = described_class::FORBIDDEN_KEY_VALUE - - expect(setting.allowed_key_types).not_to include(:ed25519) - end - end - - describe '#key_restriction_for' do - it 'returns the restriction value for recognised types' do - setting.rsa_key_restriction = 1024 - - expect(setting.key_restriction_for(:rsa)).to eq(1024) - end - - it 'allows types to be passed as a string' do - setting.rsa_key_restriction = 1024 - - expect(setting.key_restriction_for('rsa')).to eq(1024) - end - - it 'returns forbidden for unrecognised type' do - expect(setting.key_restriction_for(:foo)).to eq(described_class::FORBIDDEN_KEY_VALUE) - end - end - - describe '#allow_signup?' do - it 'returns true' do - expect(setting.allow_signup?).to be_truthy - end - - it 'returns false if signup is disabled' do - allow(setting).to receive(:signup_enabled?).and_return(false) - - expect(setting.allow_signup?).to be_falsey - end - - it 'returns false if password authentication is disabled for the web interface' do - allow(setting).to receive(:password_authentication_enabled_for_web?).and_return(false) - - expect(setting.allow_signup?).to be_falsey - end - end - - describe '#user_default_internal_regex_enabled?' do - using RSpec::Parameterized::TableSyntax - - where(:user_default_external, :user_default_internal_regex, :result) do - false | nil | false - false | '' | false - false | '^(?:(?!\.ext@).)*$\r?\n?' | false - true | '' | false - true | nil | false - true | '^(?:(?!\.ext@).)*$\r?\n?' | true - end - - with_them do - before do - setting.update(user_default_external: user_default_external) - setting.update(user_default_internal_regex: user_default_internal_regex) - end - - subject { setting.user_default_internal_regex_enabled? } - - it { is_expected.to eq(result) } - end - end - context 'diff limit settings' do describe '#diff_max_patch_bytes' do context 'validations' do @@ -613,23 +401,5 @@ describe ApplicationSetting do end end - describe '#archive_builds_older_than' do - subject { setting.archive_builds_older_than } - - context 'when the archive_builds_in_seconds is set' do - before do - setting.archive_builds_in_seconds = 3600 - end - - it { is_expected.to be_within(1.minute).of(1.hour.ago) } - end - - context 'when the archive_builds_in_seconds is set' do - before do - setting.archive_builds_in_seconds = nil - end - - it { is_expected.to be_nil } - end - end + it_behaves_like 'application settings examples' end diff --git a/spec/models/project_services/kubernetes_service_spec.rb b/spec/models/project_services/kubernetes_service_spec.rb index 47f70e6648a..cb20c747972 100644 --- a/spec/models/project_services/kubernetes_service_spec.rb +++ b/spec/models/project_services/kubernetes_service_spec.rb @@ -360,14 +360,16 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do context 'when kubernetes responds with valid pods' do before do stub_kubeclient_pods + stub_kubeclient_deployments # Used by EE end - it { is_expected.to eq(pods: [kube_pod]) } + it { is_expected.to include(pods: [kube_pod]) } end context 'when kubernetes responds with 500s' do before do stub_kubeclient_pods(status: 500) + stub_kubeclient_deployments(status: 500) # Used by EE end it { expect { subject }.to raise_error(Kubeclient::HttpError) } @@ -376,9 +378,10 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do context 'when kubernetes responds with 404s' do before do stub_kubeclient_pods(status: 404) + stub_kubeclient_deployments(status: 404) # Used by EE end - it { is_expected.to eq(pods: []) } + it { is_expected.to include(pods: []) } end end diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb index 3ccc706edf2..7be8d67ba9e 100644 --- a/spec/models/project_wiki_spec.rb +++ b/spec/models/project_wiki_spec.rb @@ -71,6 +71,14 @@ describe ProjectWiki do expect(project_wiki.create_page("index", "test content")).to be_truthy end + it "creates a new wiki repo with a default commit message" do + expect(project_wiki.create_page("index", "test content", :markdown, "")).to be_truthy + + page = project_wiki.find_page('index') + + expect(page.last_version.message).to eq("#{user.username} created page: index") + end + it "raises CouldNotCreateWikiError if it can't create the wiki repository" do # Create a fresh project which will not have a wiki project_wiki = described_class.new(create(:project), user) diff --git a/spec/policies/identity_provider_policy_spec.rb b/spec/policies/identity_provider_policy_spec.rb new file mode 100644 index 00000000000..2520469d4e7 --- /dev/null +++ b/spec/policies/identity_provider_policy_spec.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe IdentityProviderPolicy do + subject(:policy) { described_class.new(user, provider) } + let(:user) { User.new } + let(:provider) { :a_provider } + + describe '#rules' do + it { is_expected.to be_allowed(:link) } + it { is_expected.to be_allowed(:unlink) } + + context 'when user is anonymous' do + let(:user) { nil } + + it { is_expected.not_to be_allowed(:link) } + it { is_expected.not_to be_allowed(:unlink) } + end + + %w[saml cas3].each do |provider_name| + context "when provider is #{provider_name}" do + let(:provider) { provider_name } + + it { is_expected.to be_allowed(:link) } + it { is_expected.not_to be_allowed(:unlink) } + end + end + end +end diff --git a/spec/presenters/ci/pipeline_presenter_spec.rb b/spec/presenters/ci/pipeline_presenter_spec.rb index f7ceaf844be..cda07a0ae09 100644 --- a/spec/presenters/ci/pipeline_presenter_spec.rb +++ b/spec/presenters/ci/pipeline_presenter_spec.rb @@ -1,6 +1,9 @@ require 'spec_helper' describe Ci::PipelinePresenter do + include Gitlab::Routing + + let(:user) { create(:user) } let(:project) { create(:project) } let(:pipeline) { create(:ci_pipeline, project: project) } @@ -8,6 +11,11 @@ describe Ci::PipelinePresenter do described_class.new(pipeline) end + before do + project.add_developer(user) + allow(presenter).to receive(:current_user) { user } + end + it 'inherits from Gitlab::View::Presenter::Delegated' do expect(described_class.superclass).to eq(Gitlab::View::Presenter::Delegated) end @@ -68,4 +76,130 @@ describe Ci::PipelinePresenter do end end end + + describe '#ref_text' do + subject { presenter.ref_text } + + context 'when pipeline is detached merge request pipeline' do + let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline) } + let(:pipeline) { merge_request.all_pipelines.last } + + it 'returns a correct ref text' do + is_expected.to eq("for <a class=\"mr-iid\" href=\"#{project_merge_request_path(merge_request.project, merge_request)}\">#{merge_request.to_reference}</a> " \ + "with <a class=\"ref-name\" href=\"#{project_commits_path(merge_request.source_project, merge_request.source_branch)}\">#{merge_request.source_branch}</a>") + end + end + + context 'when pipeline is merge request pipeline' do + let(:merge_request) { create(:merge_request, :with_merge_request_pipeline) } + let(:pipeline) { merge_request.all_pipelines.last } + + it 'returns a correct ref text' do + is_expected.to eq("for <a class=\"mr-iid\" href=\"#{project_merge_request_path(merge_request.project, merge_request)}\">#{merge_request.to_reference}</a> " \ + "with <a class=\"ref-name\" href=\"#{project_commits_path(merge_request.source_project, merge_request.source_branch)}\">#{merge_request.source_branch}</a> " \ + "into <a class=\"ref-name\" href=\"#{project_commits_path(merge_request.target_project, merge_request.target_branch)}\">#{merge_request.target_branch}</a>") + end + end + + context 'when pipeline is branch pipeline' do + let(:pipeline) { create(:ci_pipeline, project: project) } + + context 'when ref exists in the repository' do + before do + allow(pipeline).to receive(:ref_exists?) { true } + end + + it 'returns a correct ref text' do + is_expected.to eq("for <a class=\"ref-name\" href=\"#{project_commits_path(pipeline.project, pipeline.ref)}\">#{pipeline.ref}</a>") + end + + context 'when ref contains malicious script' do + let(:pipeline) { create(:ci_pipeline, ref: "<script>alter('1')</script>", project: project) } + + it 'does not include the malicious script' do + is_expected.not_to include("<script>alter('1')</script>") + end + end + end + + context 'when ref exists in the repository' do + before do + allow(pipeline).to receive(:ref_exists?) { false } + end + + it 'returns a correct ref text' do + is_expected.to eq("for <span class=\"ref-name\">#{pipeline.ref}</span>") + end + + context 'when ref contains malicious script' do + let(:pipeline) { create(:ci_pipeline, ref: "<script>alter('1')</script>", project: project) } + + it 'does not include the malicious script' do + is_expected.not_to include("<script>alter('1')</script>") + end + end + end + end + end + + describe '#link_to_merge_request' do + subject { presenter.link_to_merge_request } + + let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline) } + let(:pipeline) { merge_request.all_pipelines.last } + + it 'returns a correct link' do + is_expected + .to include(project_merge_request_path(merge_request.project, merge_request)) + end + + context 'when pipeline is branch pipeline' do + let(:pipeline) { create(:ci_pipeline, project: project) } + + it 'returns nothing' do + is_expected.to be_nil + end + end + end + + describe '#link_to_merge_request_source_branch' do + subject { presenter.link_to_merge_request_source_branch } + + let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline) } + let(:pipeline) { merge_request.all_pipelines.last } + + it 'returns a correct link' do + is_expected + .to include(project_commits_path(merge_request.source_project, + merge_request.source_branch)) + end + + context 'when pipeline is branch pipeline' do + let(:pipeline) { create(:ci_pipeline, project: project) } + + it 'returns nothing' do + is_expected.to be_nil + end + end + end + + describe '#link_to_merge_request_target_branch' do + subject { presenter.link_to_merge_request_target_branch } + + let(:merge_request) { create(:merge_request, :with_merge_request_pipeline) } + let(:pipeline) { merge_request.all_pipelines.last } + + it 'returns a correct link' do + is_expected + .to include(project_commits_path(merge_request.target_project, merge_request.target_branch)) + end + + context 'when pipeline is branch pipeline' do + let(:pipeline) { create(:ci_pipeline, project: project) } + + it 'returns nothing' do + is_expected.to be_nil + end + end + end end diff --git a/spec/presenters/merge_request_presenter_spec.rb b/spec/presenters/merge_request_presenter_spec.rb index 02cefcbc916..fd03f594c35 100644 --- a/spec/presenters/merge_request_presenter_spec.rb +++ b/spec/presenters/merge_request_presenter_spec.rb @@ -345,6 +345,30 @@ describe MergeRequestPresenter do end end + describe '#source_branch_commits_path' do + subject do + described_class.new(resource, current_user: user) + .source_branch_commits_path + end + + context 'when source branch exists' do + it 'returns path' do + allow(resource).to receive(:source_branch_exists?) { true } + + is_expected + .to eq("/#{resource.source_project.full_path}/commits/#{resource.source_branch}") + end + end + + context 'when source branch does not exist' do + it 'returns nil' do + allow(resource).to receive(:source_branch_exists?) { false } + + is_expected.to be_nil + end + end + end + describe '#target_branch_tree_path' do subject do described_class.new(resource, current_user: user) diff --git a/spec/requests/api/search_spec.rb b/spec/requests/api/search_spec.rb index c48ca832c85..49672591b3b 100644 --- a/spec/requests/api/search_spec.rb +++ b/spec/requests/api/search_spec.rb @@ -77,6 +77,28 @@ describe API::Search do it_behaves_like 'response is correct', schema: 'public_api/v4/milestones' end + context 'for users scope' do + before do + create(:user, name: 'billy') + + get api('/search', user), params: { scope: 'users', search: 'billy' } + end + + it_behaves_like 'response is correct', schema: 'public_api/v4/user/basics' + + context 'when users search feature is disabled' do + before do + allow(Feature).to receive(:disabled?).with(:users_search, default_enabled: true).and_return(true) + + get api('/search', user), params: { scope: 'users', search: 'billy' } + end + + it 'returns 400 error' do + expect(response).to have_gitlab_http_status(400) + end + end + end + context 'for snippet_titles scope' do before do create(:snippet, :public, title: 'awesome snippet', content: 'snippet content') @@ -192,6 +214,40 @@ describe API::Search do it_behaves_like 'response is correct', schema: 'public_api/v4/milestones' end + + context 'for users scope' do + before do + user = create(:user, name: 'billy') + create(:group_member, :developer, user: user, group: group) + + get api("/groups/#{group.id}/search", user), params: { scope: 'users', search: 'billy' } + end + + it_behaves_like 'response is correct', schema: 'public_api/v4/user/basics' + + context 'when users search feature is disabled' do + before do + allow(Feature).to receive(:disabled?).with(:users_search, default_enabled: true).and_return(true) + + get api("/groups/#{group.id}/search", user), params: { scope: 'users', search: 'billy' } + end + + it 'returns 400 error' do + expect(response).to have_gitlab_http_status(400) + end + end + end + + context 'for users scope with group path as id' do + before do + user1 = create(:user, name: 'billy') + create(:group_member, :developer, user: user1, group: group) + + get api("/groups/#{CGI.escape(group.full_path)}/search", user), params: { scope: 'users', search: 'billy' } + end + + it_behaves_like 'response is correct', schema: 'public_api/v4/user/basics' + end end end @@ -269,6 +325,29 @@ describe API::Search do it_behaves_like 'response is correct', schema: 'public_api/v4/milestones' end + context 'for users scope' do + before do + user1 = create(:user, name: 'billy') + create(:project_member, :developer, user: user1, project: project) + + get api("/projects/#{project.id}/search", user), params: { scope: 'users', search: 'billy' } + end + + it_behaves_like 'response is correct', schema: 'public_api/v4/user/basics' + + context 'when users search feature is disabled' do + before do + allow(Feature).to receive(:disabled?).with(:users_search, default_enabled: true).and_return(true) + + get api("/projects/#{project.id}/search", user), params: { scope: 'users', search: 'billy' } + end + + it 'returns 400 error' do + expect(response).to have_gitlab_http_status(400) + end + end + end + context 'for notes scope' do before do create(:note_on_merge_request, project: project, note: 'awesome note') diff --git a/spec/routing/api_routing_spec.rb b/spec/routing/api_routing_spec.rb index 5fde4bd885b..3c48ead4ff2 100644 --- a/spec/routing/api_routing_spec.rb +++ b/spec/routing/api_routing_spec.rb @@ -7,25 +7,17 @@ describe 'api', 'routing' do end it 'does not route to the GraphqlController' do - expect(get('/api/graphql')).not_to route_to('graphql#execute') - end - - it 'does not expose graphiql' do - expect(get('/-/graphql-explorer')).not_to route_to('graphiql/rails/editors#show') + expect(post('/api/graphql')).not_to route_to('graphql#execute') end end - context 'when graphql is disabled' do + context 'when graphql is enabled' do before do stub_feature_flags(graphql: true) end it 'routes to the GraphqlController' do - expect(get('/api/graphql')).not_to route_to('graphql#execute') - end - - it 'exposes graphiql' do - expect(get('/-/graphql-explorer')).not_to route_to('graphiql/rails/editors#show') + expect(post('/api/graphql')).to route_to('graphql#execute') end end end diff --git a/spec/serializers/pipeline_entity_spec.rb b/spec/serializers/pipeline_entity_spec.rb index c8308a0ae85..1d992e8a483 100644 --- a/spec/serializers/pipeline_entity_spec.rb +++ b/spec/serializers/pipeline_entity_spec.rb @@ -159,13 +159,13 @@ describe PipelineEntity do expect(subject[:merge_request][:source_branch]) .to eq(merge_request.source_branch) - expect(project_branch_path(project, merge_request.source_branch)) + expect(project_commits_path(project, merge_request.source_branch)) .to include(subject[:merge_request][:source_branch_path]) expect(subject[:merge_request][:target_branch]) .to eq(merge_request.target_branch) - expect(project_branch_path(project, merge_request.target_branch)) + expect(project_commits_path(project, merge_request.target_branch)) .to include(subject[:merge_request][:target_branch_path]) end end diff --git a/spec/services/auth/container_registry_authentication_service_spec.rb b/spec/services/auth/container_registry_authentication_service_spec.rb index 8021bd338e0..c9d85e96750 100644 --- a/spec/services/auth/container_registry_authentication_service_spec.rb +++ b/spec/services/auth/container_registry_authentication_service_spec.rb @@ -88,6 +88,12 @@ describe Auth::ContainerRegistryAuthenticationService do end end + shared_examples 'a deletable since registry 2.7' do + it_behaves_like 'an accessible' do + let(:actions) { ['delete'] } + end + end + shared_examples 'a pullable' do it_behaves_like 'an accessible' do let(:actions) { ['pull'] } @@ -184,6 +190,19 @@ describe Auth::ContainerRegistryAuthenticationService do it_behaves_like 'not a container repository factory' end + context 'disallow developer to delete images since registry 2.7' do + before do + project.add_developer(current_user) + end + + let(:current_params) do + { scopes: ["repository:#{project.full_path}:delete"] } + end + + it_behaves_like 'an inaccessible' + it_behaves_like 'not a container repository factory' + end + context 'allow reporter to pull images' do before do project.add_reporter(current_user) @@ -212,6 +231,19 @@ describe Auth::ContainerRegistryAuthenticationService do it_behaves_like 'not a container repository factory' end + context 'disallow reporter to delete images since registry 2.7' do + before do + project.add_reporter(current_user) + end + + let(:current_params) do + { scopes: ["repository:#{project.full_path}:delete"] } + end + + it_behaves_like 'an inaccessible' + it_behaves_like 'not a container repository factory' + end + context 'return a least of privileges' do before do project.add_reporter(current_user) @@ -250,6 +282,19 @@ describe Auth::ContainerRegistryAuthenticationService do it_behaves_like 'an inaccessible' it_behaves_like 'not a container repository factory' end + + context 'disallow guest to delete images since regsitry 2.7' do + before do + project.add_guest(current_user) + end + + let(:current_params) do + { scopes: ["repository:#{project.full_path}:delete"] } + end + + it_behaves_like 'an inaccessible' + it_behaves_like 'not a container repository factory' + end end context 'for public project' do @@ -282,6 +327,15 @@ describe Auth::ContainerRegistryAuthenticationService do it_behaves_like 'not a container repository factory' end + context 'disallow anyone to delete images since registry 2.7' do + let(:current_params) do + { scopes: ["repository:#{project.full_path}:delete"] } + end + + it_behaves_like 'an inaccessible' + it_behaves_like 'not a container repository factory' + end + context 'when repository name is invalid' do let(:current_params) do { scopes: ['repository:invalid:push'] } @@ -322,6 +376,15 @@ describe Auth::ContainerRegistryAuthenticationService do it_behaves_like 'an inaccessible' it_behaves_like 'not a container repository factory' end + + context 'disallow anyone to delete images since registry 2.7' do + let(:current_params) do + { scopes: ["repository:#{project.full_path}:delete"] } + end + + it_behaves_like 'an inaccessible' + it_behaves_like 'not a container repository factory' + end end context 'for external user' do @@ -344,6 +407,16 @@ describe Auth::ContainerRegistryAuthenticationService do it_behaves_like 'an inaccessible' it_behaves_like 'not a container repository factory' end + + context 'disallow anyone to delete images since registry 2.7' do + let(:current_user) { create(:user, external: true) } + let(:current_params) do + { scopes: ["repository:#{project.full_path}:delete"] } + end + + it_behaves_like 'an inaccessible' + it_behaves_like 'not a container repository factory' + end end end end @@ -371,6 +444,16 @@ describe Auth::ContainerRegistryAuthenticationService do let(:project) { current_project } end end + + context 'allow to delete images since registry 2.7' do + let(:current_params) do + { scopes: ["repository:#{current_project.full_path}:delete"] } + end + + it_behaves_like 'a deletable since registry 2.7' do + let(:project) { current_project } + end + end end context 'build authorized as user' do @@ -419,6 +502,16 @@ describe Auth::ContainerRegistryAuthenticationService do end end + context 'disallow to delete images since registry 2.7' do + let(:current_params) do + { scopes: ["repository:#{current_project.full_path}:delete"] } + end + + it_behaves_like 'an inaccessible' do + let(:project) { current_project } + end + end + context 'for other projects' do context 'when pulling' do let(:current_params) do diff --git a/spec/support/helpers/javascript_fixtures_helpers.rb b/spec/support/helpers/javascript_fixtures_helpers.rb index f525b2f945e..cceb179d53e 100644 --- a/spec/support/helpers/javascript_fixtures_helpers.rb +++ b/spec/support/helpers/javascript_fixtures_helpers.rb @@ -5,7 +5,7 @@ module JavaScriptFixturesHelpers extend ActiveSupport::Concern include Gitlab::Popen - FIXTURE_PATHS = %w[spec/javascripts/fixtures ee/spec/javascripts/fixtures].freeze + extend self included do |base| base.around do |example| @@ -14,32 +14,32 @@ module JavaScriptFixturesHelpers end end + def fixture_root_path + 'spec/javascripts/fixtures' + end + # Public: Removes all fixture files from given directory # - # directory_name - directory of the fixtures (relative to FIXTURE_PATHS) + # directory_name - directory of the fixtures (relative to .fixture_root_path) # def clean_frontend_fixtures(directory_name) - FIXTURE_PATHS.each do |fixture_path| - directory_name = File.expand_path(directory_name, fixture_path) - Dir[File.expand_path('*.html.raw', directory_name)].each do |file_name| - FileUtils.rm(file_name) - end + full_directory_name = File.expand_path(directory_name, fixture_root_path) + Dir[File.expand_path('*.html.raw', full_directory_name)].each do |file_name| + FileUtils.rm(file_name) end end # Public: Store a response object as fixture file # # response - string or response object to store - # fixture_file_name - file name to store the fixture in (relative to FIXTURE_PATHS) + # fixture_file_name - file name to store the fixture in (relative to .fixture_root_path) # def store_frontend_fixture(response, fixture_file_name) - FIXTURE_PATHS.each do |fixture_path| - fixture_file_name = File.expand_path(fixture_file_name, fixture_path) - fixture = response.respond_to?(:body) ? parse_response(response) : response + full_fixture_path = File.expand_path(fixture_file_name, fixture_root_path) + fixture = response.respond_to?(:body) ? parse_response(response) : response - FileUtils.mkdir_p(File.dirname(fixture_file_name)) - File.write(fixture_file_name, fixture) - end + FileUtils.mkdir_p(File.dirname(full_fixture_path)) + File.write(full_fixture_path, fixture) end def remove_repository(project) diff --git a/spec/support/helpers/stub_configuration.rb b/spec/support/helpers/stub_configuration.rb index ff21bbe28ca..cfa9151b2d7 100644 --- a/spec/support/helpers/stub_configuration.rb +++ b/spec/support/helpers/stub_configuration.rb @@ -84,6 +84,10 @@ module StubConfiguration allow(Gitlab.config.kerberos).to receive_messages(to_settings(messages)) end + def stub_gitlab_shell_setting(messages) + allow(Gitlab.config.gitlab_shell).to receive_messages(to_settings(messages)) + end + private # Modifies stubbed messages to also stub possible predicate versions diff --git a/spec/support/shared_examples/application_setting_examples.rb b/spec/support/shared_examples/application_setting_examples.rb new file mode 100644 index 00000000000..e7ec24c5b7e --- /dev/null +++ b/spec/support/shared_examples/application_setting_examples.rb @@ -0,0 +1,252 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'application settings examples' do + context 'restricted signup domains' do + it 'sets single domain' do + setting.domain_whitelist_raw = 'example.com' + expect(setting.domain_whitelist).to eq(['example.com']) + end + + it 'sets multiple domains with spaces' do + setting.domain_whitelist_raw = 'example.com *.example.com' + expect(setting.domain_whitelist).to eq(['example.com', '*.example.com']) + end + + it 'sets multiple domains with newlines and a space' do + setting.domain_whitelist_raw = "example.com\n *.example.com" + expect(setting.domain_whitelist).to eq(['example.com', '*.example.com']) + end + + it 'sets multiple domains with commas' do + setting.domain_whitelist_raw = "example.com, *.example.com" + expect(setting.domain_whitelist).to eq(['example.com', '*.example.com']) + end + end + + context 'blacklisted signup domains' do + it 'sets single domain' do + setting.domain_blacklist_raw = 'example.com' + expect(setting.domain_blacklist).to contain_exactly('example.com') + end + + it 'sets multiple domains with spaces' do + setting.domain_blacklist_raw = 'example.com *.example.com' + expect(setting.domain_blacklist).to contain_exactly('example.com', '*.example.com') + end + + it 'sets multiple domains with newlines and a space' do + setting.domain_blacklist_raw = "example.com\n *.example.com" + expect(setting.domain_blacklist).to contain_exactly('example.com', '*.example.com') + end + + it 'sets multiple domains with commas' do + setting.domain_blacklist_raw = "example.com, *.example.com" + expect(setting.domain_blacklist).to contain_exactly('example.com', '*.example.com') + end + + it 'sets multiple domains with semicolon' do + setting.domain_blacklist_raw = "example.com; *.example.com" + expect(setting.domain_blacklist).to contain_exactly('example.com', '*.example.com') + end + + it 'sets multiple domains with mixture of everything' do + setting.domain_blacklist_raw = "example.com; *.example.com\n test.com\sblock.com yes.com" + expect(setting.domain_blacklist).to contain_exactly('example.com', '*.example.com', 'test.com', 'block.com', 'yes.com') + end + + it 'sets multiple domain with file' do + setting.domain_blacklist_file = File.open(Rails.root.join('spec/fixtures/', 'domain_blacklist.txt')) + expect(setting.domain_blacklist).to contain_exactly('example.com', 'test.com', 'foo.bar') + end + end + + describe 'usage ping settings' do + context 'when the usage ping is disabled in gitlab.yml' do + before do + allow(Settings.gitlab).to receive(:usage_ping_enabled).and_return(false) + end + + it 'does not allow the usage ping to be configured' do + expect(setting.usage_ping_can_be_configured?).to be_falsey + end + + context 'when the usage ping is disabled in the DB' do + before do + setting.usage_ping_enabled = false + end + + it 'returns false for usage_ping_enabled' do + expect(setting.usage_ping_enabled).to be_falsey + end + end + + context 'when the usage ping is enabled in the DB' do + before do + setting.usage_ping_enabled = true + end + + it 'returns false for usage_ping_enabled' do + expect(setting.usage_ping_enabled).to be_falsey + end + end + end + + context 'when the usage ping is enabled in gitlab.yml' do + before do + allow(Settings.gitlab).to receive(:usage_ping_enabled).and_return(true) + end + + it 'allows the usage ping to be configured' do + expect(setting.usage_ping_can_be_configured?).to be_truthy + end + + context 'when the usage ping is disabled in the DB' do + before do + setting.usage_ping_enabled = false + end + + it 'returns false for usage_ping_enabled' do + expect(setting.usage_ping_enabled).to be_falsey + end + end + + context 'when the usage ping is enabled in the DB' do + before do + setting.usage_ping_enabled = true + end + + it 'returns true for usage_ping_enabled' do + expect(setting.usage_ping_enabled).to be_truthy + end + end + end + end + + describe '#allowed_key_types' do + it 'includes all key types by default' do + expect(setting.allowed_key_types).to contain_exactly(*described_class::SUPPORTED_KEY_TYPES) + end + + it 'excludes disabled key types' do + expect(setting.allowed_key_types).to include(:ed25519) + + setting.ed25519_key_restriction = described_class::FORBIDDEN_KEY_VALUE + + expect(setting.allowed_key_types).not_to include(:ed25519) + end + end + + describe '#key_restriction_for' do + it 'returns the restriction value for recognised types' do + setting.rsa_key_restriction = 1024 + + expect(setting.key_restriction_for(:rsa)).to eq(1024) + end + + it 'allows types to be passed as a string' do + setting.rsa_key_restriction = 1024 + + expect(setting.key_restriction_for('rsa')).to eq(1024) + end + + it 'returns forbidden for unrecognised type' do + expect(setting.key_restriction_for(:foo)).to eq(described_class::FORBIDDEN_KEY_VALUE) + end + end + + describe '#allow_signup?' do + it 'returns true' do + expect(setting.allow_signup?).to be_truthy + end + + it 'returns false if signup is disabled' do + allow(setting).to receive(:signup_enabled?).and_return(false) + + expect(setting.allow_signup?).to be_falsey + end + + it 'returns false if password authentication is disabled for the web interface' do + allow(setting).to receive(:password_authentication_enabled_for_web?).and_return(false) + + expect(setting.allow_signup?).to be_falsey + end + end + + describe '#pick_repository_storage' do + it 'uses Array#sample to pick a random storage' do + array = double('array', sample: 'random') + expect(setting).to receive(:repository_storages).and_return(array) + + expect(setting.pick_repository_storage).to eq('random') + end + end + + describe '#user_default_internal_regex_enabled?' do + using RSpec::Parameterized::TableSyntax + + where(:user_default_external, :user_default_internal_regex, :result) do + false | nil | false + false | '' | false + false | '^(?:(?!\.ext@).)*$\r?\n?' | false + true | '' | false + true | nil | false + true | '^(?:(?!\.ext@).)*$\r?\n?' | true + end + + with_them do + before do + setting.user_default_external = user_default_external + setting.user_default_internal_regex = user_default_internal_regex + end + + subject { setting.user_default_internal_regex_enabled? } + + it { is_expected.to eq(result) } + end + end + + describe '#archive_builds_older_than' do + subject { setting.archive_builds_older_than } + + context 'when the archive_builds_in_seconds is set' do + before do + setting.archive_builds_in_seconds = 3600 + end + + it { is_expected.to be_within(1.minute).of(1.hour.ago) } + end + + context 'when the archive_builds_in_seconds is set' do + before do + setting.archive_builds_in_seconds = nil + end + + it { is_expected.to be_nil } + end + end + + describe '#commit_email_hostname' do + context 'when the value is provided' do + before do + setting.commit_email_hostname = 'localhost' + end + + it 'returns the provided value' do + expect(setting.commit_email_hostname).to eq('localhost') + end + end + + context 'when the value is not provided' do + it 'returns the default from the class' do + expect(setting.commit_email_hostname) + .to eq(described_class.default_commit_email_hostname) + end + end + end + + it 'predicate method changes when value is updated' do + setting.password_authentication_enabled_for_web = false + + expect(setting.password_authentication_enabled_for_web?).to be_falsey + end +end diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb index a8fae4a88a3..bdbd39475b9 100644 --- a/spec/tasks/gitlab/backup_rake_spec.rb +++ b/spec/tasks/gitlab/backup_rake_spec.rb @@ -21,9 +21,6 @@ describe 'gitlab:app namespace rake task' do # empty task as env is already loaded Rake::Task.define_task :environment - - # We need this directory to run `gitlab:backup:create` task - FileUtils.mkdir_p('public/uploads') end before do @@ -38,6 +35,7 @@ describe 'gitlab:app namespace rake task' do end def run_rake_task(task_name) + FileUtils.mkdir_p('tmp/tests/public/uploads') Rake::Task[task_name].reenable Rake.application.invoke_task task_name end diff --git a/spec/tasks/gitlab/storage_rake_spec.rb b/spec/tasks/gitlab/storage_rake_spec.rb index 736809eee5b..4b04d9cec39 100644 --- a/spec/tasks/gitlab/storage_rake_spec.rb +++ b/spec/tasks/gitlab/storage_rake_spec.rb @@ -89,9 +89,9 @@ describe 'rake gitlab:storage:*', :sidekiq do describe 'gitlab:storage:migrate_to_hashed' do let(:task) { 'gitlab:storage:migrate_to_hashed' } - context 'with rollback already scheduled' do + context 'with rollback already scheduled', :redis do it 'does nothing' do - Sidekiq::Testing.fake! do + Sidekiq::Testing.disable! do ::HashedStorage::RollbackerWorker.perform_async(1, 5) expect(Project).not_to receive(:with_unmigrated_storage) @@ -146,9 +146,9 @@ describe 'rake gitlab:storage:*', :sidekiq do it_behaves_like 'make sure database is writable' - context 'with migration already scheduled' do + context 'with migration already scheduled', :redis do it 'does nothing' do - Sidekiq::Testing.fake! do + Sidekiq::Testing.disable! do ::HashedStorage::MigratorWorker.perform_async(1, 5) expect(Project).not_to receive(:with_unmigrated_storage) diff --git a/yarn.lock b/yarn.lock index dbd56b387ba..15107a64991 100644 --- a/yarn.lock +++ b/yarn.lock @@ -663,10 +663,10 @@ resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.54.0.tgz#00320e845efd46716042cde0c348b990d4908daf" integrity sha512-DR17iy8TM5IbXEacqiDP0p8SuC/J8EL+98xbfVz5BKvRsPHpeZJQNlBF/petIV5d+KWM5A9v3GZTY7uMU7z/JQ== -"@gitlab/ui@^2.2.3": - version "2.2.3" - resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-2.2.3.tgz#b3b4d1d785662dfba44ad2a7354fcbe4cee22fc9" - integrity sha512-N1Q1O6zpS4zZhmvWsUCtuGXTzQeHOzRWQZctbFTEJonidIWk6juqIBduYgR0MadG3DZxiovUN12jDGVtCfZKzw== +"@gitlab/ui@^2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-2.3.0.tgz#7840aa4ad6638a90e82fa99f3a5dcac1785f7477" + integrity sha512-7hH+Q6SeP0hMMM21TQoGmvNjBcadgD+gWlGcKlnN1euH+6kfmOT5TCdrvsUjsZSNdycSXrEMMcQYy2oXG1sbdw== dependencies: "@babel/standalone" "^7.0.0" bootstrap-vue "^2.0.0-rc.11" |