diff options
101 files changed, 1250 insertions, 558 deletions
@@ -420,7 +420,7 @@ gem 'vmstat', '~> 2.3.0' gem 'sys-filesystem', '~> 1.1.6' # SSH host key support -gem 'net-ssh', '~> 5.0' +gem 'net-ssh', '~> 5.2' gem 'sshkey', '~> 2.0' # Required for ED25519 SSH host key support diff --git a/Gemfile.lock b/Gemfile.lock index c8b8f0e4f90..85b4c32f168 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -523,7 +523,7 @@ GEM mysql2 (0.4.10) nakayoshi_fork (0.0.4) net-ldap (0.16.0) - net-ssh (5.0.1) + net-ssh (5.2.0) netrc (0.11.0) nio4r (2.3.1) nokogiri (1.10.3) @@ -1152,7 +1152,7 @@ DEPENDENCIES mysql2 (~> 0.4.10) nakayoshi_fork (~> 0.0.4) net-ldap - net-ssh (~> 5.0) + net-ssh (~> 5.2) nokogiri (~> 1.10.3) oauth2 (~> 1.4) octokit (~> 4.9) diff --git a/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue b/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue index 29d2cca6aed..99d77a75c23 100644 --- a/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue +++ b/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue @@ -126,10 +126,6 @@ export default { {{ __('No forks available to you.') }}<br /> <span v-html="noForkText"></span> </template> - <gl-link :href="helpPagePath" class="help-link" target="_blank"> - <span class="sr-only">{{ __('Read more') }}</span> - <i class="fa fa-question-circle" aria-hidden="true"></i> - </gl-link> </p> </div> </div> diff --git a/app/assets/javascripts/issuable_bulk_update_actions.js b/app/assets/javascripts/issuable_bulk_update_actions.js index bc9d7fcf30d..c855f3973b0 100644 --- a/app/assets/javascripts/issuable_bulk_update_actions.js +++ b/app/assets/javascripts/issuable_bulk_update_actions.js @@ -1,4 +1,4 @@ -/* eslint-disable consistent-return, func-names, array-callback-return, prefer-arrow-callback, no-unused-vars */ +/* eslint-disable consistent-return, func-names, array-callback-return, prefer-arrow-callback */ import $ from 'jquery'; import _ from 'underscore'; @@ -7,7 +7,7 @@ import Flash from './flash'; import { __ } from './locale'; export default { - init({ container, form, issues, prefixId } = {}) { + init({ form, issues, prefixId } = {}) { this.prefixId = prefixId || 'issue_'; this.form = form || this.getElement('.bulk-update'); this.$labelDropdown = this.form.find('.js-label-select'); diff --git a/app/assets/javascripts/issuable_index.js b/app/assets/javascripts/issuable_index.js index 16f88cddce3..f3f8b6ec715 100644 --- a/app/assets/javascripts/issuable_index.js +++ b/app/assets/javascripts/issuable_index.js @@ -2,26 +2,13 @@ import $ from 'jquery'; import axios from './lib/utils/axios_utils'; import flash from './flash'; import { s__, __ } from './locale'; -import IssuableBulkUpdateSidebar from './issuable_bulk_update_sidebar'; -import IssuableBulkUpdateActions from './issuable_bulk_update_actions'; +import issuableInitBulkUpdateSidebar from './issuable_init_bulk_update_sidebar'; export default class IssuableIndex { constructor(pagePrefix) { - this.initBulkUpdate(pagePrefix); + issuableInitBulkUpdateSidebar.init(pagePrefix); IssuableIndex.resetIncomingEmailToken(); } - initBulkUpdate(pagePrefix) { - const userCanBulkUpdate = $('.issues-bulk-update').length > 0; - const alreadyInitialized = Boolean(this.bulkUpdateSidebar); - - if (userCanBulkUpdate && !alreadyInitialized) { - IssuableBulkUpdateActions.init({ - prefixId: pagePrefix, - }); - - this.bulkUpdateSidebar = new IssuableBulkUpdateSidebar(); - } - } static resetIncomingEmailToken() { const $resetToken = $('.incoming-email-token-reset'); diff --git a/app/assets/javascripts/issuable_init_bulk_update_sidebar.js b/app/assets/javascripts/issuable_init_bulk_update_sidebar.js new file mode 100644 index 00000000000..da8969c80f3 --- /dev/null +++ b/app/assets/javascripts/issuable_init_bulk_update_sidebar.js @@ -0,0 +1,19 @@ +import IssuableBulkUpdateSidebar from './issuable_bulk_update_sidebar'; +import issuableBulkUpdateActions from './issuable_bulk_update_actions'; + +export default { + bulkUpdateSidebar: null, + + init(prefixId) { + const bulkUpdateEl = document.querySelector('.issues-bulk-update'); + const alreadyInitialized = Boolean(this.bulkUpdateSidebar); + + if (bulkUpdateEl && !alreadyInitialized) { + issuableBulkUpdateActions.init({ prefixId }); + + this.bulkUpdateSidebar = new IssuableBulkUpdateSidebar(); + } + + return this.bulkUpdateSidebar; + }, +}; diff --git a/app/assets/javascripts/pages/groups/issues/index.js b/app/assets/javascripts/pages/groups/issues/index.js index 23fb5656008..dcdee77a8ab 100644 --- a/app/assets/javascripts/pages/groups/issues/index.js +++ b/app/assets/javascripts/pages/groups/issues/index.js @@ -1,11 +1,15 @@ import projectSelect from '~/project_select'; import initFilteredSearch from '~/pages/search/init_filtered_search'; +import issuableInitBulkUpdateSidebar from '~/issuable_init_bulk_update_sidebar'; import { FILTERED_SEARCH } from '~/pages/constants'; import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys'; import initManualOrdering from '~/manual_ordering'; +const ISSUE_BULK_UPDATE_PREFIX = 'issue_'; + document.addEventListener('DOMContentLoaded', () => { IssuableFilteredSearchTokenKeys.addExtraTokensForIssues(); + issuableInitBulkUpdateSidebar.init(ISSUE_BULK_UPDATE_PREFIX); initFilteredSearch({ page: FILTERED_SEARCH.ISSUES, diff --git a/app/assets/javascripts/pages/projects/issues/form.js b/app/assets/javascripts/pages/projects/issues/form.js index 941c4552579..2205a7bafe3 100644 --- a/app/assets/javascripts/pages/projects/issues/form.js +++ b/app/assets/javascripts/pages/projects/issues/form.js @@ -17,7 +17,5 @@ export default () => { new MilestoneSelect(); new IssuableTemplateSelectors(); - if (gon.features.graphql) { - initSuggestions(); - } + initSuggestions(); }; diff --git a/app/assets/javascripts/vue_shared/directives/tooltip.js b/app/assets/javascripts/vue_shared/directives/tooltip.js index 2d1f7a1cfd0..73e92728cb9 100644 --- a/app/assets/javascripts/vue_shared/directives/tooltip.js +++ b/app/assets/javascripts/vue_shared/directives/tooltip.js @@ -3,8 +3,12 @@ import '~/commons/bootstrap'; export default { bind(el) { + const glTooltipDelay = localStorage.getItem('gl-tooltip-delay'); + const delay = glTooltipDelay ? JSON.parse(glTooltipDelay) : 0; + $(el).tooltip({ trigger: 'hover', + delay, }); }, diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index a12029d2419..e75c1379dfb 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -287,8 +287,8 @@ list-style: none; padding: 0 1px; - a:not(.help-link), - button:not(.btn), + a, + button:not(.dropdown-toggle,.ci-action-icon-container), .menu-item { @include dropdown-link; } diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb index 065d2d3a4ec..6fa2f75be33 100644 --- a/app/controllers/concerns/issuable_actions.rb +++ b/app/controllers/concerns/issuable_actions.rb @@ -92,7 +92,7 @@ module IssuableActions end def bulk_update - result = Issuable::BulkUpdateService.new(project, current_user, bulk_update_params).execute(resource_name) + result = Issuable::BulkUpdateService.new(current_user, bulk_update_params).execute(resource_name) quantity = result[:count] render json: { notice: "#{quantity} #{resource_name.pluralize(quantity)} updated" } @@ -181,7 +181,7 @@ module IssuableActions end def authorize_admin_issuable! - unless can?(current_user, :"admin_#{resource_name}", @project) # rubocop:disable Gitlab/ModuleWithInstanceVariables + unless can?(current_user, :"admin_#{resource_name}", parent) return access_denied! end end diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb index 1ce0afac83b..9fbbe373b0d 100644 --- a/app/controllers/graphql_controller.rb +++ b/app/controllers/graphql_controller.rb @@ -11,7 +11,6 @@ class GraphqlController < ApplicationController # around in GraphiQL. protect_from_forgery with: :null_session, only: :execute - before_action :check_graphql_feature_flag! before_action :authorize_access_api! before_action(only: [:execute]) { authenticate_sessionless_user!(:api) } @@ -86,8 +85,4 @@ class GraphqlController < ApplicationController render json: error, status: status end - - def check_graphql_feature_flag! - render_404 unless Gitlab::Graphql.enabled? - end end diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index b866f574f67..228de8bc6f3 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -45,8 +45,6 @@ class Projects::IssuesController < Projects::ApplicationController before_action :authorize_import_issues!, only: [:import_csv] before_action :authorize_download_code!, only: [:related_branches] - before_action :set_suggested_issues_feature_flags, only: [:new] - respond_to :html def index @@ -285,8 +283,4 @@ class Projects::IssuesController < Projects::ApplicationController # 3. https://gitlab.com/gitlab-org/gitlab-ce/issues/42426 Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42422') end - - def set_suggested_issues_feature_flags - push_frontend_feature_flag(:graphql, default_enabled: true) - end end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 8dee842a22d..8d0079a4dd3 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -662,6 +662,6 @@ module ProjectsHelper end def vue_file_list_enabled? - Gitlab::Graphql.enabled? && Feature.enabled?(:vue_file_list, @project) + Feature.enabled?(:vue_file_list, @project) end end diff --git a/app/helpers/storage_helper.rb b/app/helpers/storage_helper.rb index ecf37bae6b3..ce810433a3a 100644 --- a/app/helpers/storage_helper.rb +++ b/app/helpers/storage_helper.rb @@ -17,6 +17,6 @@ module StorageHelper counter_lfs_objects: storage_counter(statistics.lfs_objects_size) } - _("%{counter_repositories} repositories, %{counter_wikis} wikis, %{counter_build_artifacts} build artifacts, %{counter_lfs_objects} LFS") % counters + _("Repository: %{counter_repositories} / Wikis: %{counter_wikis} / Build Artifacts: %{counter_build_artifacts} / LFS: %{counter_lfs_objects}") % counters end end diff --git a/app/models/concerns/project_api_compatibility.rb b/app/models/concerns/project_api_compatibility.rb new file mode 100644 index 00000000000..cb00efb06df --- /dev/null +++ b/app/models/concerns/project_api_compatibility.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +# Add methods used by the projects API +module ProjectAPICompatibility + extend ActiveSupport::Concern + + def build_git_strategy=(value) + write_attribute(:build_allow_git_fetch, value == 'fetch') + end + + def auto_devops_enabled=(value) + self.build_auto_devops if self.auto_devops&.enabled.nil? + self.auto_devops.update! enabled: value + end + + def auto_devops_deploy_strategy=(value) + self.build_auto_devops if self.auto_devops&.enabled.nil? + self.auto_devops.update! deploy_strategy: value + end +end diff --git a/app/models/concerns/project_features_compatibility.rb b/app/models/concerns/project_features_compatibility.rb index f268a842db4..551a2e56ecf 100644 --- a/app/models/concerns/project_features_compatibility.rb +++ b/app/models/concerns/project_features_compatibility.rb @@ -9,32 +9,70 @@ require 'gitlab/utils' module ProjectFeaturesCompatibility extend ActiveSupport::Concern + # TODO: remove in API v5, replaced by *_access_level def wiki_enabled=(value) - write_feature_attribute(:wiki_access_level, value) + write_feature_attribute_boolean(:wiki_access_level, value) end + # TODO: remove in API v5, replaced by *_access_level def builds_enabled=(value) - write_feature_attribute(:builds_access_level, value) + write_feature_attribute_boolean(:builds_access_level, value) end + # TODO: remove in API v5, replaced by *_access_level def merge_requests_enabled=(value) - write_feature_attribute(:merge_requests_access_level, value) + write_feature_attribute_boolean(:merge_requests_access_level, value) end + # TODO: remove in API v5, replaced by *_access_level def issues_enabled=(value) - write_feature_attribute(:issues_access_level, value) + write_feature_attribute_boolean(:issues_access_level, value) end + # TODO: remove in API v5, replaced by *_access_level def snippets_enabled=(value) - write_feature_attribute(:snippets_access_level, value) + write_feature_attribute_boolean(:snippets_access_level, value) + end + + def repository_access_level=(value) + write_feature_attribute_string(:repository_access_level, value) + end + + def wiki_access_level=(value) + write_feature_attribute_string(:wiki_access_level, value) + end + + def builds_access_level=(value) + write_feature_attribute_string(:builds_access_level, value) + end + + def merge_requests_access_level=(value) + write_feature_attribute_string(:merge_requests_access_level, value) + end + + def issues_access_level=(value) + write_feature_attribute_string(:issues_access_level, value) + end + + def snippets_access_level=(value) + write_feature_attribute_string(:snippets_access_level, value) end private - def write_feature_attribute(field, value) + def write_feature_attribute_boolean(field, value) + access_level = Gitlab::Utils.to_boolean(value) ? ProjectFeature::ENABLED : ProjectFeature::DISABLED + write_feature_attribute_raw(field, access_level) + end + + def write_feature_attribute_string(field, value) + access_level = ProjectFeature.access_level_from_str(value) + write_feature_attribute_raw(field, access_level) + end + + def write_feature_attribute_raw(field, value) build_project_feature unless project_feature - access_level = Gitlab::Utils.to_boolean(value) ? ProjectFeature::ENABLED : ProjectFeature::DISABLED - project_feature.__send__(:write_attribute, field, access_level) # rubocop:disable GitlabSecurity/PublicSend + project_feature.__send__(:write_attribute, field, value) # rubocop:disable GitlabSecurity/PublicSend end end diff --git a/app/models/label_note.rb b/app/models/label_note.rb index d6814f4a948..ba5f1f82a81 100644 --- a/app/models/label_note.rb +++ b/app/models/label_note.rb @@ -62,19 +62,27 @@ class LabelNote < Note end def note_text(html: false) - added = labels_str('added', label_refs_by_action('add', html)) - removed = labels_str('removed', label_refs_by_action('remove', html)) + added = labels_str(label_refs_by_action('add', html), prefix: 'added', suffix: added_suffix) + removed = labels_str(label_refs_by_action('remove', html), prefix: removed_prefix) [added, removed].compact.join(' and ') end + def removed_prefix + 'removed' + end + + def added_suffix + '' + end + # returns string containing added/removed labels including # count of deleted labels: # # added ~1 ~2 + 1 deleted label # added 3 deleted labels # added ~1 ~2 labels - def labels_str(prefix, label_refs) + def labels_str(label_refs, prefix: '', suffix: '') existing_refs = label_refs.select { |ref| ref.present? }.sort refs_str = existing_refs.empty? ? nil : existing_refs.join(' ') @@ -84,9 +92,9 @@ class LabelNote < Note return unless refs_str || deleted_str label_list_str = [refs_str, deleted_str].compact.join(' + ') - suffix = 'label'.pluralize(deleted > 0 ? deleted : existing_refs.count) + suffix += ' label'.pluralize(deleted > 0 ? deleted : existing_refs.count) - "#{prefix} #{label_list_str} #{suffix}" + "#{prefix} #{label_list_str} #{suffix.squish}" end def label_refs_by_action(action, html) diff --git a/app/models/project.rb b/app/models/project.rb index 0f4fba5d0b6..bfc35b77b8f 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -15,6 +15,7 @@ class Project < ApplicationRecord include CaseSensitivity include TokenAuthenticatable include ValidAttribute + include ProjectAPICompatibility include ProjectFeaturesCompatibility include SelectForProjectAuthorization include Presentable @@ -2144,7 +2145,7 @@ class Project < ApplicationRecord public? && repository_exists? && Gitlab::CurrentSettings.hashed_storage_enabled && - Feature.enabled?(:object_pools, self) + Feature.enabled?(:object_pools, self, default_enabled: true) end def leave_pool_repository diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb index 0542581c6e0..7ff06655de0 100644 --- a/app/models/project_feature.rb +++ b/app/models/project_feature.rb @@ -24,6 +24,12 @@ class ProjectFeature < ApplicationRecord FEATURES = %i(issues merge_requests wiki snippets builds repository pages).freeze PRIVATE_FEATURES_MIN_ACCESS_LEVEL = { merge_requests: Gitlab::Access::REPORTER }.freeze + STRING_OPTIONS = HashWithIndifferentAccess.new({ + 'disabled' => DISABLED, + 'private' => PRIVATE, + 'enabled' => ENABLED, + 'public' => PUBLIC + }).freeze class << self def access_level_attribute(feature) @@ -45,6 +51,14 @@ class ProjectFeature < ApplicationRecord PRIVATE_FEATURES_MIN_ACCESS_LEVEL.fetch(feature, Gitlab::Access::GUEST) end + def access_level_from_str(level) + STRING_OPTIONS.fetch(level) + end + + def str_from_access_level(level) + STRING_OPTIONS.key(level) + end + private def ensure_feature!(feature) @@ -83,6 +97,10 @@ class ProjectFeature < ApplicationRecord public_send(ProjectFeature.access_level_attribute(feature)) # rubocop:disable GitlabSecurity/PublicSend end + def string_access_level(feature) + ProjectFeature.str_from_access_level(access_level(feature)) + end + def builds_enabled? builds_access_level > DISABLED end diff --git a/app/models/resource_label_event.rb b/app/models/resource_label_event.rb index f2c7cb6a65d..ad08f4763ae 100644 --- a/app/models/resource_label_event.rb +++ b/app/models/resource_label_event.rb @@ -36,10 +36,9 @@ class ResourceLabelEvent < ApplicationRecord issue || merge_request end - # create same discussion id for all actions with the same user and time def discussion_id(resource = nil) strong_memoize(:discussion_id) do - Digest::SHA1.hexdigest([self.class.name, created_at, user_id].join("-")) + Digest::SHA1.hexdigest(discussion_id_key.join("-")) end end @@ -121,4 +120,8 @@ class ResourceLabelEvent < ApplicationRecord def resource_parent issuable.project || issuable.group end + + def discussion_id_key + [self.class.name, created_at, user_id] + end end diff --git a/app/services/issuable/bulk_update_service.rb b/app/services/issuable/bulk_update_service.rb index c4beddf2294..6d215d7a3b9 100644 --- a/app/services/issuable/bulk_update_service.rb +++ b/app/services/issuable/bulk_update_service.rb @@ -1,7 +1,15 @@ # frozen_string_literal: true module Issuable - class BulkUpdateService < IssuableBaseService + class BulkUpdateService + include Gitlab::Allowable + + attr_accessor :current_user, :params + + def initialize(user = nil, params = {}) + @current_user, @params = user, params.dup + end + # rubocop: disable CodeReuse/ActiveRecord def execute(type) model_class = type.classify.constantize diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index 02de080e0ba..db673cace81 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -182,7 +182,7 @@ class IssuableBaseService < BaseService # To be overridden by subclasses end - def before_update(issuable) + def before_update(issuable, skip_spam_check: false) # To be overridden by subclasses end @@ -257,7 +257,7 @@ class IssuableBaseService < BaseService last_edited_at: Time.now, last_edited_by: current_user)) - before_update(issuable) + before_update(issuable, skip_spam_check: true) if issuable.with_transaction_returning_status { issuable.save } # We do not touch as it will affect a update on updated_at field diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb index 6b9f23f24cd..7cd825aa967 100644 --- a/app/services/issues/update_service.rb +++ b/app/services/issues/update_service.rb @@ -17,8 +17,8 @@ module Issues super end - def before_update(issue) - spam_check(issue, current_user) + def before_update(issue, skip_spam_check: false) + spam_check(issue, current_user) unless skip_spam_check end def handle_changes(issue, options) diff --git a/app/services/merge_requests/merge_base_service.rb b/app/services/merge_requests/merge_base_service.rb index 095bdca5472..1ed396cee1e 100644 --- a/app/services/merge_requests/merge_base_service.rb +++ b/app/services/merge_requests/merge_base_service.rb @@ -28,6 +28,17 @@ module MergeRequests private + def check_source + unless source + raise_error('No source for merge') + end + end + + # Overridden in EE. + def check_size_limit + # No-op + end + # Overridden in EE. def error_check! # No-op diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb index d8a78001b79..3e0f5aa181c 100644 --- a/app/services/merge_requests/merge_service.rb +++ b/app/services/merge_requests/merge_service.rb @@ -48,13 +48,13 @@ module MergeRequests def error_check! super + check_source + error = if @merge_request.should_be_rebased? 'Only fast-forward merge is allowed for your project. Please update your source branch' elsif !@merge_request.mergeable? 'Merge request is not mergeable' - elsif !source - 'No source for merge' end raise_error(error) if error diff --git a/app/services/merge_requests/merge_to_ref_service.rb b/app/services/merge_requests/merge_to_ref_service.rb index 0ea50a5dbf5..37b5805ae7e 100644 --- a/app/services/merge_requests/merge_to_ref_service.rb +++ b/app/services/merge_requests/merge_to_ref_service.rb @@ -16,7 +16,7 @@ module MergeRequests def execute(merge_request) @merge_request = merge_request - validate! + error_check! commit_id = commit @@ -39,21 +39,9 @@ module MergeRequests merge_request.diff_head_sha end - def validate! - error_check! - end - + override :error_check! def error_check! - super - - error = - if !hooks_validation_pass?(merge_request) - hooks_validation_error(merge_request) - elsif source.blank? - 'No source for merge' - end - - raise_error(error) if error + check_source end ## diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml index f524d35d79e..98230684d56 100644 --- a/app/views/admin/groups/show.html.haml +++ b/app/views/admin/groups/show.html.haml @@ -43,11 +43,7 @@ = render_if_exists 'admin/namespace_plan_info', namespace: @group %li - %span.light= _('Storage:') - %strong= storage_counter(@group.storage_size) - ( - = storage_counters_details(@group) - ) + = render 'shared/storage_counter_statistics', storage_size: @group.storage_size, storage_details: @group %li %span.light= _('Group Git LFS status:') diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml index e23accc1ea9..0fae8060b32 100644 --- a/app/views/admin/projects/show.html.haml +++ b/app/views/admin/projects/show.html.haml @@ -73,11 +73,7 @@ = @project.repository.relative_path %li - %span.light= _('Storage:') - %strong= storage_counter(@project.statistics&.storage_size) - - if @project.statistics - = surround '(', ')' do - = storage_counters_details(@project.statistics) + = render 'shared/storage_counter_statistics', storage_size: @project.statistics&.storage_size, storage_details: @project.statistics %li %span.light last commit: diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml index 91d17cfd745..f05e269553a 100644 --- a/app/views/groups/issues.html.haml +++ b/app/views/groups/issues.html.haml @@ -1,3 +1,5 @@ +- @can_bulk_update = can?(current_user, :admin_issue, @group) + - page_title "Issues" = content_for :meta_tags do = auto_discovery_link_tag(:atom, safe_params.merge(rss_url_options).to_h, title: "#{@group.name} issues") @@ -9,8 +11,15 @@ = render 'shared/issuable/nav', type: :issues .nav-controls = render 'shared/issuable/feed_buttons' + + - if @can_bulk_update + = render_if_exists 'shared/issuable/bulk_update_button' + = render 'shared/new_project_item_select', path: 'issues/new', label: "New issue", type: :issues, with_feature_enabled: 'issues', with_shared: false, include_projects_in_subgroups: true = render 'shared/issuable/search_bar', type: :issues + - if @can_bulk_update + = render_if_exists 'shared/issuable/group_bulk_update_sidebar', group: @group, type: :issues + = render 'shared/issues' diff --git a/app/views/shared/_storage_counter_statistics.html.haml b/app/views/shared/_storage_counter_statistics.html.haml new file mode 100644 index 00000000000..99b2323ca82 --- /dev/null +++ b/app/views/shared/_storage_counter_statistics.html.haml @@ -0,0 +1,4 @@ +%span.light= _('Storage:') +%strong= storage_counter(storage_size) +- if storage_details + (#{storage_counters_details(storage_details)}) diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index 1bd56e064d5..214e87052da 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -17,8 +17,7 @@ = render 'shared/issuable/form/template_selector', issuable: issuable = render 'shared/issuable/form/title', issuable: issuable, form: form, has_wip_commits: commits && commits.detect(&:work_in_progress?) -- if Gitlab::Graphql.enabled? - #js-suggestions{ data: { project_path: @project.full_path } } +#js-suggestions{ data: { project_path: @project.full_path } } = render 'shared/form_elements/description', model: issuable, form: form, project: project diff --git a/changelogs/unreleased/61284-frontend-follow-up-from-add-packages_size-to-projectstatistics.yml b/changelogs/unreleased/61284-frontend-follow-up-from-add-packages_size-to-projectstatistics.yml new file mode 100644 index 00000000000..3445057f7fb --- /dev/null +++ b/changelogs/unreleased/61284-frontend-follow-up-from-add-packages_size-to-projectstatistics.yml @@ -0,0 +1,5 @@ +--- +title: Improved readability of storage statistics in group / project admin area +merge_request: 30406 +author: +type: other diff --git a/changelogs/unreleased/63945-update-mixin-deep-to-1-3-2.yml b/changelogs/unreleased/63945-update-mixin-deep-to-1-3-2.yml new file mode 100644 index 00000000000..a0ef34f3700 --- /dev/null +++ b/changelogs/unreleased/63945-update-mixin-deep-to-1-3-2.yml @@ -0,0 +1,5 @@ +--- +title: Update mixin-deep to 1.3.2 +merge_request: 30223 +author: Takuya Noguchi +type: other diff --git a/changelogs/unreleased/api-doc-negative-commit-message-push-rule.yml b/changelogs/unreleased/api-doc-negative-commit-message-push-rule.yml new file mode 100644 index 00000000000..0500978a2e1 --- /dev/null +++ b/changelogs/unreleased/api-doc-negative-commit-message-push-rule.yml @@ -0,0 +1,5 @@ +--- +title: "Document the negative commit message push rule for the API." +merge_request: 14004 +author: Maikel Vlasman +type: added diff --git a/changelogs/unreleased/fix-median-counting-for-cycle-analytics.yml b/changelogs/unreleased/fix-median-counting-for-cycle-analytics.yml new file mode 100644 index 00000000000..6ae6db08ba1 --- /dev/null +++ b/changelogs/unreleased/fix-median-counting-for-cycle-analytics.yml @@ -0,0 +1,5 @@ +--- +title: Fix median counting for cycle analytics +merge_request: 30229 +author: +type: fixed diff --git a/changelogs/unreleased/issue_64021.yml b/changelogs/unreleased/issue_64021.yml new file mode 100644 index 00000000000..088d9348619 --- /dev/null +++ b/changelogs/unreleased/issue_64021.yml @@ -0,0 +1,5 @@ +--- +title: Skip spam check for task list updates +merge_request: 30279 +author: +type: fixed diff --git a/changelogs/unreleased/jramsay-enable-object-dedupe-by-default.yml b/changelogs/unreleased/jramsay-enable-object-dedupe-by-default.yml new file mode 100644 index 00000000000..b953d7c0fc8 --- /dev/null +++ b/changelogs/unreleased/jramsay-enable-object-dedupe-by-default.yml @@ -0,0 +1,5 @@ +--- +title: Enable Git object pools +merge_request: 29595 +author: jramsay +type: changed diff --git a/changelogs/unreleased/patch-29.yml b/changelogs/unreleased/patch-29.yml new file mode 100644 index 00000000000..674c06e1259 --- /dev/null +++ b/changelogs/unreleased/patch-29.yml @@ -0,0 +1,5 @@ +--- +title: Updates PHP template to php:latest to ensure always targeting latest stable +merge_request: 30319 +author: Paul Giberson +type: changed diff --git a/changelogs/unreleased/project_api.yml b/changelogs/unreleased/project_api.yml new file mode 100644 index 00000000000..a04f9bb5608 --- /dev/null +++ b/changelogs/unreleased/project_api.yml @@ -0,0 +1,5 @@ +--- +title: Improve Project API +merge_request: 28327 +author: Mathieu Parent +type: added diff --git a/config/application.rb b/config/application.rb index c5ef6a2c60d..edf8b3e87f9 100644 --- a/config/application.rb +++ b/config/application.rb @@ -6,6 +6,7 @@ Bundler.require(:default, Rails.env) module Gitlab class Application < Rails::Application + require_dependency Rails.root.join('lib/gitlab') require_dependency Rails.root.join('lib/gitlab/redis/wrapper') require_dependency Rails.root.join('lib/gitlab/redis/cache') require_dependency Rails.root.join('lib/gitlab/redis/queues') diff --git a/config/initializers/0_license.rb b/config/initializers/0_license.rb new file mode 100644 index 00000000000..f750022dfdf --- /dev/null +++ b/config/initializers/0_license.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +Gitlab.ee do + begin + public_key_file = File.read(Rails.root.join(".license_encryption_key.pub")) + public_key = OpenSSL::PKey::RSA.new(public_key_file) + Gitlab::License.encryption_key = public_key + rescue + warn "WARNING: No valid license encryption key provided." + end + + # Needed to run migration + if ActiveRecord::Base.connected? && ActiveRecord::Base.connection.data_source_exists?('licenses') + message = LicenseHelper.license_message(signed_in: true, is_admin: true, in_html: false) + if ::License.block_changes? && message.present? + warn "WARNING: #{message}" + end + end +end diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index bf187e9a282..0b8a6607250 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -76,6 +76,7 @@ Gitlab.ee do Settings['smartcard'] ||= Settingslogic.new({}) Settings.smartcard['enabled'] = false if Settings.smartcard['enabled'].nil? Settings.smartcard['client_certificate_required_port'] = 3444 if Settings.smartcard['client_certificate_required_port'].nil? + Settings.smartcard['required_for_git_access'] = false if Settings.smartcard['required_for_git_access'].nil? end Settings['omniauth'] ||= Settingslogic.new({}) diff --git a/config/initializers/console_message.rb b/config/initializers/console_message.rb index 05eb395028d..04c109aa844 100644 --- a/config/initializers/console_message.rb +++ b/config/initializers/console_message.rb @@ -2,9 +2,18 @@ if defined?(Rails::Console) # note that this will not print out when using `spring` justify = 15 - puts "-------------------------------------------------------------------------------------" + + puts '-' * 80 puts " GitLab:".ljust(justify) + "#{Gitlab::VERSION} (#{Gitlab.revision})" puts " GitLab Shell:".ljust(justify) + "#{Gitlab::VersionInfo.parse(Gitlab::Shell.new.version)}" puts " #{Gitlab::Database.human_adapter_name}:".ljust(justify) + Gitlab::Database.version - puts "-------------------------------------------------------------------------------------" + + Gitlab.ee do + if Gitlab::Geo.enabled? + puts " Geo enabled:".ljust(justify) + 'yes' + puts " Geo server:".ljust(justify) + EE::GeoHelper.current_node_human_status + end + end + + puts '-' * 80 end diff --git a/config/initializers/elastic_client_setup.rb b/config/initializers/elastic_client_setup.rb new file mode 100644 index 00000000000..2ecb7956007 --- /dev/null +++ b/config/initializers/elastic_client_setup.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +# Be sure to restart your server when you modify this file. + +require 'gitlab/current_settings' + +Gitlab.ee do + Elasticsearch::Model::Response::Records.prepend GemExtensions::Elasticsearch::Model::Response::Records + Elasticsearch::Model::Adapter::Multiple::Records.prepend GemExtensions::Elasticsearch::Model::Adapter::Multiple::Records + Elasticsearch::Model::Indexing::InstanceMethods.prepend GemExtensions::Elasticsearch::Model::Indexing::InstanceMethods + + module Elasticsearch + module Model + module Client + # This mutex is only used to synchronize *creation* of a new client, so + # all including classes can share the same client instance + CLIENT_MUTEX = Mutex.new + + cattr_accessor :cached_client + cattr_accessor :cached_config + + module ClassMethods + # Override the default ::Elasticsearch::Model::Client implementation to + # return a client configured from application settings. All including + # classes will use the same instance, which is refreshed automatically + # if the settings change. + # + # _client is present to match the arity of the overridden method, where + # it is also not used. + # + # @return [Elasticsearch::Transport::Client] + def client(_client = nil) + store = ::Elasticsearch::Model::Client + + store::CLIENT_MUTEX.synchronize do + config = Gitlab::CurrentSettings.elasticsearch_config + + if store.cached_client.nil? || config != store.cached_config + store.cached_client = ::Gitlab::Elastic::Client.build(config) + store.cached_config = config + end + end + + store.cached_client + end + end + end + end + end +end diff --git a/config/initializers/geo.rb b/config/initializers/geo.rb new file mode 100644 index 00000000000..4cc9fbf49b2 --- /dev/null +++ b/config/initializers/geo.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +Gitlab.ee do + if File.exist?(Rails.root.join('config/database_geo.yml')) + Rails.application.configure do + config.geo_database = config_for(:database_geo) + end + end + + begin + if Gitlab::Geo.connected? && Gitlab::Geo.primary? + Gitlab::Geo.current_node&.update_clone_url! + end + rescue => e + warn "WARNING: Unable to check/update clone_url_prefix for Geo: #{e}" + end +end diff --git a/config/initializers/health_check.rb b/config/initializers/health_check.rb index 959daa93f78..9f466dc39de 100644 --- a/config/initializers/health_check.rb +++ b/config/initializers/health_check.rb @@ -1,4 +1,10 @@ HealthCheck.setup do |config| config.standard_checks = %w(database migrations cache) config.full_checks = %w(database migrations cache) + + Gitlab.ee do + config.add_custom_check('geo') do + Gitlab::Geo::HealthCheck.new.perform_checks + end + end end diff --git a/config/initializers/load_balancing.rb b/config/initializers/load_balancing.rb new file mode 100644 index 00000000000..029c0ff4277 --- /dev/null +++ b/config/initializers/load_balancing.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +# We need to run this initializer after migrations are done so it doesn't fail on CI + +Gitlab.ee do + if ActiveRecord::Base.connected? && ActiveRecord::Base.connection.data_source_exists?('licenses') + if Gitlab::Database::LoadBalancing.enable? + Gitlab::Database.disable_prepared_statements + + Gitlab::Application.configure do |config| + config.middleware.use(Gitlab::Database::LoadBalancing::RackMiddleware) + end + + Gitlab::Database::LoadBalancing.configure_proxy + + # This needs to be executed after fork of clustered processes + Gitlab::Cluster::LifecycleEvents.on_worker_start do + # Service discovery must be started after configuring the proxy, as service + # discovery depends on this. + Gitlab::Database::LoadBalancing.start_service_discovery + end + + end + end +end diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb index 7b69cf11288..f9ef5d66bfa 100644 --- a/config/initializers/sidekiq.rb +++ b/config/initializers/sidekiq.rb @@ -85,6 +85,19 @@ Sidekiq.configure_server do |config| ActiveRecord::Base.establish_connection(db_config) Rails.logger.debug("Connection Pool size for Sidekiq Server is now: #{ActiveRecord::Base.connection.pool.instance_variable_get('@size')}") + Gitlab.ee do + Gitlab::Mirror.configure_cron_job! + + Gitlab::Geo.configure_cron_jobs! + + if Gitlab::Geo.geo_database_configured? + Rails.configuration.geo_database['pool'] = Sidekiq.options[:concurrency] + Geo::TrackingBase.establish_connection(Rails.configuration.geo_database) + + Rails.logger.debug("Connection Pool size for Sidekiq Server is now: #{Geo::TrackingBase.connection_pool.size} (Geo tracking database)") + end + end + # Avoid autoload issue such as 'Mail::Parsers::AddressStruct' # https://github.com/mikel/mail/issues/912#issuecomment-214850355 Mail.eager_autoload! diff --git a/config/initializers/sidekiq_cluster.rb b/config/initializers/sidekiq_cluster.rb new file mode 100644 index 00000000000..baa7495aa29 --- /dev/null +++ b/config/initializers/sidekiq_cluster.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +if ENV['ENABLE_SIDEKIQ_CLUSTER'] && Gitlab.ee? + Thread.new do + Thread.current.abort_on_exception = true + + parent = Process.ppid + + loop do + sleep(5) + + # In cluster mode it's possible that the master process is SIGKILL'd. In + # this case the parent PID changes and we need to terminate ourselves. + if Process.ppid != parent + Process.kill(:TERM, Process.pid) + break + end + end + end +end diff --git a/config/routes/api.rb b/config/routes/api.rb index 3ba9176d943..c6c17b5708e 100644 --- a/config/routes/api.rb +++ b/config/routes/api.rb @@ -1,7 +1,5 @@ -constraints(::Constraints::FeatureConstrainer.new(:graphql, default_enabled: true)) do - post '/api/graphql', to: 'graphql#execute' - mount GraphiQL::Rails::Engine, at: '/-/graphql-explorer', graphql_path: '/api/graphql' -end +post '/api/graphql', to: 'graphql#execute' +mount GraphiQL::Rails::Engine, at: '/-/graphql-explorer', graphql_path: '/api/graphql' ::API::API.logger Rails.logger mount ::API::API => '/' diff --git a/doc/administration/audit_events.md b/doc/administration/audit_events.md index ae1f6e00ea5..a80ff330e03 100644 --- a/doc/administration/audit_events.md +++ b/doc/administration/audit_events.md @@ -99,7 +99,7 @@ It is possible to filter particular actions by choosing an audit data type from the filter drop-down. You can further filter by specific group, project or user (for authentication events). -![audit log](audit_log.png) +![audit log](img/audit_log.png) ### Missing events diff --git a/doc/administration/auditor_users.md b/doc/administration/auditor_users.md index daba5b4ca82..65d36612d85 100644 --- a/doc/administration/auditor_users.md +++ b/doc/administration/auditor_users.md @@ -52,7 +52,7 @@ section. **Admin Area > Users**. You will find the option of the access level under the 'Access' section. - ![Admin Area Form](auditor_access_form.png) + ![Admin Area Form](img/auditor_access_form.png) 1. Click **Save changes** or **Create user** for the changes to take effect. diff --git a/doc/administration/audit_log.png b/doc/administration/img/audit_log.png Binary files differindex d4f4c2abf38..d4f4c2abf38 100644 --- a/doc/administration/audit_log.png +++ b/doc/administration/img/audit_log.png diff --git a/doc/administration/auditor_access_form.png b/doc/administration/img/auditor_access_form.png Binary files differindex c179a7d3b0a..c179a7d3b0a 100644 --- a/doc/administration/auditor_access_form.png +++ b/doc/administration/img/auditor_access_form.png diff --git a/doc/administration/repository_storage_types.md b/doc/administration/repository_storage_types.md index f4cb89c84a4..9dea6074a3f 100644 --- a/doc/administration/repository_storage_types.md +++ b/doc/administration/repository_storage_types.md @@ -80,25 +80,20 @@ 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. +> [Introduced](https://gitlab.com/gitlab-org/gitaly/issues/1606) in GitLab 12.1. -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. +Forks of public projects are deduplicated by creating a third repository, the object pool, containing the objects from the source project. Using `objects/info/alternates`, the source project and forks use the object pool for shared objects. Objects are moved from the source project to the object pool when housekeeping is run on the source project. ```ruby # object pool paths "@pools/#{hash[0..1]}/#{hash[2..3]}/#{hash}.git" ``` -The object pool feature is behind the `object_pools` feature flag, and can be -enabled for individual projects by executing -`Feature.enable(:object_pools, Project.find(<id>))`. Note that the project has to -be on hashed storage, should not be a fork itself, and hashed storage should be -enabled for all new projects. +Object pools can be disabled using the `object_pools` feature flag, and can be +disabled for individual projects by executing +`Feature.disable(:object_pools, Project.find(<id>))`. Disabling object pools +will not change existing deduplicated forks, but will prevent new forks from +being deduplicated. DANGER: **Danger:** Do not run `git prune` or `git gc` in pool repositories! This can diff --git a/doc/api/graphql/index.md b/doc/api/graphql/index.md index 3bf96e279b7..db073321808 100644 --- a/doc/api/graphql/index.md +++ b/doc/api/graphql/index.md @@ -1,4 +1,4 @@ -# GraphQL API (Alpha) +# GraphQL API > [Introduced][ce-19008] in GitLab 11.0. @@ -23,23 +23,13 @@ programmatically with GitLab. To achieve this, it needs full coverage - anything possible in the REST API should also be possible in the GraphQL API. To help us meet this vision, the frontend should use GraphQL in preference to -the REST API for new features, although the alpha status of GraphQL may prevent -this from being a possibility at times. +the REST API for new features. There are no plans to deprecate the REST API. To reduce the technical burden of supporting two APIs in parallel, they should share implementations as much as possible. -## Enabling the GraphQL feature - -The GraphQL API itself is currently in Alpha, and therefore hidden behind a -feature flag. You can enable the feature using the [features api][features-api] on a self-hosted instance. - -For example: - -```shell -curl --data "value=100" --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/features/graphql -``` +As of the 12.1 release, GraphQL is always enabled. ## Available queries diff --git a/doc/api/projects.md b/doc/api/projects.md index da05b090699..c199babc1be 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -708,11 +708,17 @@ POST /projects | `namespace_id` | integer | no | Namespace for the new project (defaults to the current user's namespace) | | `default_branch` | string | no | `master` by default | | `description` | string | no | Short project description | -| `issues_enabled` | boolean | no | Enable issues for this project | -| `merge_requests_enabled` | boolean | no | Enable merge requests for this project | -| `jobs_enabled` | boolean | no | Enable jobs for this project | -| `wiki_enabled` | boolean | no | Enable wiki for this project | -| `snippets_enabled` | boolean | no | Enable snippets for this project | +| `issues_enabled` | boolean | no | (deprecated) Enable issues for this project. Use `issues_access_level` instead | +| `merge_requests_enabled` | boolean | no | (deprecated) Enable merge requests for this project. Use `merge_requests_access_level` instead | +| `jobs_enabled` | boolean | no | (deprecated) Enable jobs for this project. Use `builds_access_level` instead | +| `wiki_enabled` | boolean | no | (deprecated) Enable wiki for this project. Use `wiki_access_level` instead | +| `snippets_enabled` | boolean | no | (deprecated) Enable snippets for this project. Use `snippets_access_level` instead | +| `issues_access_level` | string | no | One of `disabled`, `private` or `enabled` | +| `repository_access_level` | string | no | One of `disabled`, `private` or `enabled` | +| `merge_requests_access_level` | string | no | One of `disabled`, `private` or `enabled` | +| `builds_access_level` | string | no | One of `disabled`, `private` or `enabled` | +| `wiki_access_level` | string | no | One of `disabled`, `private` or `enabled` | +| `snippets_access_level` | string | no | One of `disabled`, `private` or `enabled` | | `resolve_outdated_diff_discussions` | boolean | no | Automatically resolve merge request diffs discussions on lines changed with a push | | `container_registry_enabled` | boolean | no | Enable container registry for this project | | `shared_runners_enabled` | boolean | no | Enable shared runners for this project | @@ -727,7 +733,13 @@ POST /projects | `tag_list` | array | no | The list of tags for a project; put array of tags, that should be finally assigned to a project | | `avatar` | mixed | no | Image file for avatar of the project | | `printing_merge_request_link_enabled` | boolean | no | Show link to create/view merge request when pushing from the command line | +| `build_git_strategy` | string | no | The Git strategy. Defaults to `fetch` | +| `build_timeout` | integer | no | The maximum amount of time in minutes that a job is able run (in seconds) | +| `auto_cancel_pending_pipelines` | string | no | Auto-cancel pending pipelines (Note: this is not a boolean, but enabled/disabled | +| `build_coverage_regex` | string | no | Test coverage parsing | | `ci_config_path` | string | no | The path to CI config file | +| `auto_devops_enabled` | boolean | no | Enable Auto DevOps for this project | +| `auto_devops_deploy_strategy` | string | no | Auto Deploy strategy (`continuous`, `manual` or `timed_incremental`) | | `repository_storage` | string | no | Which storage shard the repository is on. Available only to admins | | `approvals_before_merge` | integer | no | **(STARTER)** How many approvers should approve merge requests by default | | `mirror` | boolean | no | **(STARTER)** Enables pull mirroring in a project | @@ -753,11 +765,17 @@ POST /projects/user/:user_id | `path` | string | no | Custom repository name for new project. By default generated based on name | | `namespace_id` | integer | no | Namespace for the new project (defaults to the current user's namespace) | | `description` | string | no | Short project description | -| `issues_enabled` | boolean | no | Enable issues for this project | -| `merge_requests_enabled` | boolean | no | Enable merge requests for this project | -| `jobs_enabled` | boolean | no | Enable jobs for this project | -| `wiki_enabled` | boolean | no | Enable wiki for this project | -| `snippets_enabled` | boolean | no | Enable snippets for this project | +| `issues_enabled` | boolean | no | (deprecated) Enable issues for this project. Use `issues_access_level` instead | +| `merge_requests_enabled` | boolean | no | (deprecated) Enable merge requests for this project. Use `merge_requests_access_level` instead | +| `jobs_enabled` | boolean | no | (deprecated) Enable jobs for this project. Use `builds_access_level` instead | +| `wiki_enabled` | boolean | no | (deprecated) Enable wiki for this project. Use `wiki_access_level` instead | +| `snippets_enabled` | boolean | no | (deprecated) Enable snippets for this project. Use `snippets_access_level` instead | +| `issues_access_level` | string | no | One of `disabled`, `private` or `enabled` | +| `repository_access_level` | string | no | One of `disabled`, `private` or `enabled` | +| `merge_requests_access_level` | string | no | One of `disabled`, `private` or `enabled` | +| `builds_access_level` | string | no | One of `disabled`, `private` or `enabled` | +| `wiki_access_level` | string | no | One of `disabled`, `private` or `enabled` | +| `snippets_access_level` | string | no | One of `disabled`, `private` or `enabled` | | `resolve_outdated_diff_discussions` | boolean | no | Automatically resolve merge request diffs discussions on lines changed with a push | | `container_registry_enabled` | boolean | no | Enable container registry for this project | | `shared_runners_enabled` | boolean | no | Enable shared runners for this project | @@ -772,7 +790,13 @@ POST /projects/user/:user_id | `tag_list` | array | no | The list of tags for a project; put array of tags, that should be finally assigned to a project | | `avatar` | mixed | no | Image file for avatar of the project | | `printing_merge_request_link_enabled` | boolean | no | Show link to create/view merge request when pushing from the command line | +| `build_git_strategy` | string | no | The Git strategy. Defaults to `fetch` | +| `build_timeout` | integer | no | The maximum amount of time in minutes that a job is able run (in seconds) | +| `auto_cancel_pending_pipelines` | string | no | Auto-cancel pending pipelines (Note: this is not a boolean, but enabled/disabled | +| `build_coverage_regex` | string | no | Test coverage parsing | | `ci_config_path` | string | no | The path to CI config file | +| `auto_devops_enabled` | boolean | no | Enable Auto DevOps for this project | +| `auto_devops_deploy_strategy` | string | no | Auto Deploy strategy (`continuous`, `manual` or `timed_incremental`) | | `repository_storage` | string | no | Which storage shard the repository is on. Available only to admins | | `approvals_before_merge` | integer | no | **(STARTER)** How many approvers should approve merge requests by default | | `external_authorization_classification_label` | string | no | **(CORE ONLY)** The classification label for the project | @@ -798,11 +822,17 @@ PUT /projects/:id | `path` | string | no | Custom repository name for the project. By default generated based on name | | `default_branch` | string | no | `master` by default | | `description` | string | no | Short project description | -| `issues_enabled` | boolean | no | Enable issues for this project | -| `merge_requests_enabled` | boolean | no | Enable merge requests for this project | -| `jobs_enabled` | boolean | no | Enable jobs for this project | -| `wiki_enabled` | boolean | no | Enable wiki for this project | -| `snippets_enabled` | boolean | no | Enable snippets for this project | +| `issues_enabled` | boolean | no | (deprecated) Enable issues for this project. Use `issues_access_level` instead | +| `merge_requests_enabled` | boolean | no | (deprecated) Enable merge requests for this project. Use `merge_requests_access_level` instead | +| `jobs_enabled` | boolean | no | (deprecated) Enable jobs for this project. Use `builds_access_level` instead | +| `wiki_enabled` | boolean | no | (deprecated) Enable wiki for this project. Use `wiki_access_level` instead | +| `snippets_enabled` | boolean | no | (deprecated) Enable snippets for this project. Use `snippets_access_level` instead | +| `issues_access_level` | string | no | One of `disabled`, `private` or `enabled` | +| `repository_access_level` | string | no | One of `disabled`, `private` or `enabled` | +| `merge_requests_access_level` | string | no | One of `disabled`, `private` or `enabled` | +| `builds_access_level` | string | no | One of `disabled`, `private` or `enabled` | +| `wiki_access_level` | string | no | One of `disabled`, `private` or `enabled` | +| `snippets_access_level` | string | no | One of `disabled`, `private` or `enabled` | | `resolve_outdated_diff_discussions` | boolean | no | Automatically resolve merge request diffs discussions on lines changed with a push | | `container_registry_enabled` | boolean | no | Enable container registry for this project | | `shared_runners_enabled` | boolean | no | Enable shared runners for this project | @@ -816,8 +846,14 @@ PUT /projects/:id | `request_access_enabled` | boolean | no | Allow users to request member access | | `tag_list` | array | no | The list of tags for a project; put array of tags, that should be finally assigned to a project | | `avatar` | mixed | no | Image file for avatar of the project | +| `build_git_strategy` | string | no | The Git strategy. Defaults to `fetch` | +| `build_timeout` | integer | no | The maximum amount of time in minutes that a job is able run (in seconds) | +| `auto_cancel_pending_pipelines` | string | no | Auto-cancel pending pipelines (Note: this is not a boolean, but enabled/disabled | +| `build_coverage_regex` | string | no | Test coverage parsing | | `ci_config_path` | string | no | The path to CI config file | | `ci_default_git_depth` | integer | no | Default number of revisions for [shallow cloning](../user/project/pipelines/settings.md#git-shallow-clone) | +| `auto_devops_enabled` | boolean | no | Enable Auto DevOps for this project | +| `auto_devops_deploy_strategy` | string | no | Auto Deploy strategy (`continuous`, `manual` or `timed_incremental`) | | `repository_storage` | string | no | Which storage shard the repository is on. Available only to admins | | `approvals_before_merge` | integer | no | **(STARTER)** How many approvers should approve merge request by default | | `external_authorization_classification_label` | string | no | **(CORE ONLY)** The classification label for the project | @@ -1631,6 +1667,7 @@ GET /projects/:id/push_rule "id": 1, "project_id": 3, "commit_message_regex": "Fixes \d+\..*", + "commit_message_negative_regex": "ssh\:\/\/", "branch_name_regex": "", "deny_delete_tag": false, "created_at": "2012-10-12T17:04:47Z", @@ -1663,18 +1700,19 @@ Adds a push rule to a specified project. POST /projects/:id/push_rule ``` -| Attribute | Type | Required | Description | -| -------------------------------------- | -------------- | -------- | ----------- | -| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME | -| `deny_delete_tag` **(STARTER)** | boolean | no | Deny deleting a tag | -| `member_check` **(STARTER)** | boolean | no | Restrict commits by author (email) to existing GitLab users | -| `prevent_secrets` **(STARTER)** | boolean | no | GitLab will reject any files that are likely to contain secrets | -| `commit_message_regex` **(STARTER)** | string | no | All commit messages must match this, e.g. `Fixed \d+\..*` | -| `branch_name_regex` **(STARTER)** | string | no | All branch names must match this, e.g. `(feature|hotfix)\/*` | -| `author_email_regex` **(STARTER)** | string | no | All commit author emails must match this, e.g. `@my-company.com$` | -| `file_name_regex` **(STARTER)** | string | no | All commited filenames must **not** match this, e.g. `(jar|exe)$` | -| `max_file_size` **(STARTER)** | integer | no | Maximum file size (MB) | -| `commit_committer_check` **(PREMIUM)** | boolean | no | Users can only push commits to this repository that were committed with one of their own verified emails. | +| Attribute | Type | Required | Description | +| --------------------------------------------- | -------------- | -------- | ----------- | +| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME | +| `deny_delete_tag` **(STARTER)** | boolean | no | Deny deleting a tag | +| `member_check` **(STARTER)** | boolean | no | Restrict commits by author (email) to existing GitLab users | +| `prevent_secrets` **(STARTER)** | boolean | no | GitLab will reject any files that are likely to contain secrets | +| `commit_message_regex` **(STARTER)** | string | no | All commit messages must match this, e.g. `Fixed \d+\..*` | +| `commit_message_negative_regex` **(STARTER)** | string | no | No commit message is allowed to match this, e.g. `ssh\:\/\/` | +| `branch_name_regex` **(STARTER)** | string | no | All branch names must match this, e.g. `(feature|hotfix)\/*` | +| `author_email_regex` **(STARTER)** | string | no | All commit author emails must match this, e.g. `@my-company.com$` | +| `file_name_regex` **(STARTER)** | string | no | All commited filenames must **not** match this, e.g. `(jar|exe)$` | +| `max_file_size` **(STARTER)** | integer | no | Maximum file size (MB) | +| `commit_committer_check` **(PREMIUM)** | boolean | no | Users can only push commits to this repository that were committed with one of their own verified emails. | ### Edit project push rule @@ -1684,18 +1722,19 @@ Edits a push rule for a specified project. PUT /projects/:id/push_rule ``` -| Attribute | Type | Required | Description | -| -------------------------------------- | -------------- | -------- | ----------- | -| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME | -| `deny_delete_tag` **(STARTER)** | boolean | no | Deny deleting a tag | -| `member_check` **(STARTER)** | boolean | no | Restrict commits by author (email) to existing GitLab users | -| `prevent_secrets` **(STARTER)** | boolean | no | GitLab will reject any files that are likely to contain secrets | -| `commit_message_regex` **(STARTER)** | string | no | All commit messages must match this, e.g. `Fixed \d+\..*` | -| `branch_name_regex` **(STARTER)** | string | no | All branch names must match this, e.g. `(feature|hotfix)\/*` | -| `author_email_regex` **(STARTER)** | string | no | All commit author emails must match this, e.g. `@my-company.com$` | -| `file_name_regex` **(STARTER)** | string | no | All commited filenames must **not** match this, e.g. `(jar|exe)$` | -| `max_file_size` **(STARTER)** | integer | no | Maximum file size (MB) | -| `commit_committer_check` **(PREMIUM)** | boolean | no | Users can only push commits to this repository that were committed with one of their own verified emails. | +| Attribute | Type | Required | Description | +| --------------------------------------------- | -------------- | -------- | ----------- | +| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME | +| `deny_delete_tag` **(STARTER)** | boolean | no | Deny deleting a tag | +| `member_check` **(STARTER)** | boolean | no | Restrict commits by author (email) to existing GitLab users | +| `prevent_secrets` **(STARTER)** | boolean | no | GitLab will reject any files that are likely to contain secrets | +| `commit_message_regex` **(STARTER)** | string | no | All commit messages must match this, e.g. `Fixed \d+\..*` | +| `commit_message_negative_regex` **(STARTER)** | string | no | No commit message is allowed to match this, e.g. `ssh\:\/\/` | +| `branch_name_regex` **(STARTER)** | string | no | All branch names must match this, e.g. `(feature|hotfix)\/*` | +| `author_email_regex` **(STARTER)** | string | no | All commit author emails must match this, e.g. `@my-company.com$` | +| `file_name_regex` **(STARTER)** | string | no | All commited filenames must **not** match this, e.g. `(jar|exe)$` | +| `max_file_size` **(STARTER)** | integer | no | Maximum file size (MB) | +| `commit_committer_check` **(PREMIUM)** | boolean | no | Users can only push commits to this repository that were committed with one of their own verified emails. | ### Delete project push rule diff --git a/doc/user/project/packages/npm_registry.md b/doc/user/project/packages/npm_registry.md index b2cfe10836f..481b1ce0337 100644 --- a/doc/user/project/packages/npm_registry.md +++ b/doc/user/project/packages/npm_registry.md @@ -11,11 +11,6 @@ project can have its own space to store NPM packages. NOTE: **Note:** Only [scoped](https://docs.npmjs.com/misc/scope) packages are supported. - -NOTE: **Note:** -As `@group/subgroup/project` is not a valid NPM package name, publishing a package -within a subgroup is not supported yet. - ## Enabling the NPM Registry NOTE: **Note:** @@ -36,12 +31,15 @@ get familiar with the package naming convention. ## Package naming convention -**Only packages that have the same path as the project** are supported. For - example: +**Packages must be scoped in the root namespace of the project**. The package +name may be anything but it is preferred that the project name be used unless +it is not possible due to a naming collision. For example: | Project | Package | Supported | | ---------------------- | ----------------------- | --------- | | `foo/bar` | `@foo/bar` | Yes | +| `foo/bar/baz` | `@foo/baz` | Yes | +| `foo/bar/buz` | `@foo/anything` | Yes | | `gitlab-org/gitlab-ce` | `@gitlab-org/gitlab-ce` | Yes | | `gitlab-org/gitlab-ce` | `@foo/bar` | No | @@ -113,6 +111,9 @@ npm publish You can then navigate to your project's **Packages** page and see the uploaded packages or even delete them. +If you attempt to publish a package with a name that already exists within +a given scope, you will receive a `403 Forbidden!` error. + ## Uploading a package with the same version twice If you upload a package with a same name and version twice, GitLab will show diff --git a/doc/user/project/settings/import_export.md b/doc/user/project/settings/import_export.md index 819515d7a4c..98bcc7cc09f 100644 --- a/doc/user/project/settings/import_export.md +++ b/doc/user/project/settings/import_export.md @@ -74,6 +74,13 @@ The following items will NOT be exported: - CI variables - Webhooks - Any encrypted tokens +- Merge Request Approvers +- Push Rules +- Awards + +NOTE: **Note:** +For more details on the specific data persisted in a project export, see the +[`import_export.yml`](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/lib/gitlab/import_export/import_export.yml) file. ## Exporting a project and its data diff --git a/lib/api/entities.rb b/lib/api/entities.rb index b9aa387ba61..5eae87fea02 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -201,6 +201,7 @@ module API # MR describing the solution: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/20555 projects_relation.preload(:project_feature, :route) .preload(:import_state, :tags) + .preload(:auto_devops) .preload(namespace: [:route, :owner]) end # rubocop: enable CodeReuse/ActiveRecord @@ -247,12 +248,20 @@ module API expose :container_registry_enabled # Expose old field names with the new permissions methods to keep API compatible + # TODO: remove in API v5, replaced by *_access_level expose(:issues_enabled) { |project, options| project.feature_available?(:issues, options[:current_user]) } expose(:merge_requests_enabled) { |project, options| project.feature_available?(:merge_requests, options[:current_user]) } expose(:wiki_enabled) { |project, options| project.feature_available?(:wiki, options[:current_user]) } expose(:jobs_enabled) { |project, options| project.feature_available?(:builds, options[:current_user]) } expose(:snippets_enabled) { |project, options| project.feature_available?(:snippets, options[:current_user]) } + expose(:issues_access_level) { |project, options| project.project_feature.string_access_level(:issues) } + expose(:repository_access_level) { |project, options| project.project_feature.string_access_level(:repository) } + expose(:merge_requests_access_level) { |project, options| project.project_feature.string_access_level(:merge_requests) } + expose(:wiki_access_level) { |project, options| project.project_feature.string_access_level(:wiki) } + expose(:builds_access_level) { |project, options| project.project_feature.string_access_level(:builds) } + expose(:snippets_access_level) { |project, options| project.project_feature.string_access_level(:snippets) } + expose :shared_runners_enabled expose :lfs_enabled?, as: :lfs_enabled expose :creator_id @@ -267,6 +276,12 @@ module API expose :runners_token, if: lambda { |_project, options| options[:user_can_admin_project] } expose :ci_default_git_depth expose :public_builds, as: :public_jobs + expose :build_git_strategy, if: lambda { |project, options| options[:user_can_admin_project] } do |project, options| + project.build_allow_git_fetch ? 'fetch' : 'clone' + end + expose :build_timeout + expose :auto_cancel_pending_pipelines + expose :build_coverage_regex expose :ci_config_path, if: -> (project, options) { Ability.allowed?(options[:current_user], :download_code, project) } expose :shared_with_groups do |project, options| SharedGroup.represent(project.project_group_links, options) @@ -280,6 +295,10 @@ module API options[:statistics] && Ability.allowed?(options[:current_user], :read_statistics, project) } expose :external_authorization_classification_label + expose :auto_devops_enabled?, as: :auto_devops_enabled + expose :auto_devops_deploy_strategy do |project, options| + project.auto_devops.nil? ? 'continuous' : project.auto_devops.deploy_strategy + end # rubocop: disable CodeReuse/ActiveRecord def self.preload_relation(projects_relation, options = {}) @@ -289,6 +308,7 @@ module API # MR describing the solution: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/20555 super(projects_relation).preload(:group) .preload(:ci_cd_settings) + .preload(:auto_devops) .preload(project_group_links: { group: :route }, fork_network: :root_project, fork_network_member: :forked_from_project, diff --git a/lib/api/helpers/graphql_helpers.rb b/lib/api/helpers/graphql_helpers.rb index 94010ab1bc2..bd60470fbd6 100644 --- a/lib/api/helpers/graphql_helpers.rb +++ b/lib/api/helpers/graphql_helpers.rb @@ -7,8 +7,6 @@ module API # should be in app/graphql/ or lib/gitlab/graphql/ module GraphqlHelpers def conditionally_graphql!(fallback:, query:, context: {}, transform: nil) - return fallback.call unless Feature.enabled?(:graphql) - result = GitlabSchema.execute(query, context: context) if transform diff --git a/lib/api/helpers/projects_helpers.rb b/lib/api/helpers/projects_helpers.rb index f242f1fea0e..0e21a7a66fd 100644 --- a/lib/api/helpers/projects_helpers.rb +++ b/lib/api/helpers/projects_helpers.rb @@ -8,12 +8,26 @@ module API params :optional_project_params_ce do optional :description, type: String, desc: 'The description of the project' + optional :build_git_strategy, type: String, values: %w(fetch clone), desc: 'The Git strategy. Defaults to `fetch`' + optional :build_timeout, type: Integer, desc: 'Build timeout' + optional :auto_cancel_pending_pipelines, type: String, values: %w(disabled enabled), desc: 'Auto-cancel pending pipelines' + optional :build_coverage_regex, type: String, desc: 'Test coverage parsing' optional :ci_config_path, type: String, desc: 'The path to CI config file. Defaults to `.gitlab-ci.yml`' + + # TODO: remove in API v5, replaced by *_access_level optional :issues_enabled, type: Boolean, desc: 'Flag indication if the issue tracker is enabled' optional :merge_requests_enabled, type: Boolean, desc: 'Flag indication if merge requests are enabled' optional :wiki_enabled, type: Boolean, desc: 'Flag indication if the wiki is enabled' optional :jobs_enabled, type: Boolean, desc: 'Flag indication if jobs are enabled' optional :snippets_enabled, type: Boolean, desc: 'Flag indication if snippets are enabled' + + optional :issues_access_level, type: String, values: %w(disabled private enabled), desc: 'Issues access level. One of `disabled`, `private` or `enabled`' + optional :repository_access_level, type: String, values: %w(disabled private enabled), desc: 'Repository access level. One of `disabled`, `private` or `enabled`' + optional :merge_requests_access_level, type: String, values: %w(disabled private enabled), desc: 'Merge requests access level. One of `disabled`, `private` or `enabled`' + optional :wiki_access_level, type: String, values: %w(disabled private enabled), desc: 'Wiki access level. One of `disabled`, `private` or `enabled`' + optional :builds_access_level, type: String, values: %w(disabled private enabled), desc: 'Builds access level. One of `disabled`, `private` or `enabled`' + optional :snippets_access_level, type: String, values: %w(disabled private enabled), desc: 'Snippets access level. One of `disabled`, `private` or `enabled`' + optional :shared_runners_enabled, type: Boolean, desc: 'Flag indication if shared runners are enabled for that project' optional :resolve_outdated_diff_discussions, type: Boolean, desc: 'Automatically resolve merge request diffs discussions on lines changed with a push' optional :container_registry_enabled, type: Boolean, desc: 'Flag indication if the container registry is enabled for that project' @@ -30,6 +44,8 @@ module API optional :initialize_with_readme, type: Boolean, desc: "Initialize a project with a README.md" optional :external_authorization_classification_label, type: String, desc: 'The classification label for the project' optional :ci_default_git_depth, type: Integer, desc: 'Default number of revisions for shallow cloning' + optional :auto_devops_enabled, type: Boolean, desc: 'Flag indication if Auto DevOps is enabled' + optional :auto_devops_deploy_strategy, type: String, values: %w(continuous manual timed_incremental), desc: 'Auto Deploy strategy' end params :optional_project_params_ee do @@ -48,15 +64,20 @@ module API def self.update_params_at_least_one_of [ - :jobs_enabled, - :resolve_outdated_diff_discussions, + :auto_devops_enabled, + :auto_devops_deploy_strategy, + :auto_cancel_pending_pipelines, + :build_coverage_regex, + :build_git_strategy, + :build_timeout, + :builds_access_level, :ci_config_path, :container_registry_enabled, :default_branch, :description, - :issues_enabled, + :issues_access_level, :lfs_enabled, - :merge_requests_enabled, + :merge_requests_access_level, :merge_method, :name, :only_allow_merge_if_all_discussions_are_resolved, @@ -64,14 +85,24 @@ module API :path, :printing_merge_request_link_enabled, :public_builds, + :repository_access_level, :request_access_enabled, + :resolve_outdated_diff_discussions, :shared_runners_enabled, - :snippets_enabled, + :snippets_access_level, :tag_list, :visibility, - :wiki_enabled, + :wiki_access_level, :avatar, - :external_authorization_classification_label + :external_authorization_classification_label, + + # TODO: remove in API v5, replaced by *_access_level + :issues_enabled, + :jobs_enabled, + :merge_requests_enabled, + :wiki_enabled, + :jobs_enabled, + :snippets_enabled ] end end diff --git a/lib/gitlab/ci/templates/PHP.gitlab-ci.yml b/lib/gitlab/ci/templates/PHP.gitlab-ci.yml index b9fee2d5731..25ea20e454f 100644 --- a/lib/gitlab/ci/templates/PHP.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/PHP.gitlab-ci.yml @@ -1,5 +1,5 @@ # Select image from https://hub.docker.com/_/php/ -image: php:7.1.1 +image: php:latest # Select what we should cache between builds cache: diff --git a/lib/gitlab/database/median.rb b/lib/gitlab/database/median.rb index 1455e410d4b..b8d895dee7d 100644 --- a/lib/gitlab/database/median.rb +++ b/lib/gitlab/database/median.rb @@ -158,7 +158,7 @@ module Gitlab Arel::Nodes::Window.new.order(arel_table[column_sym]) ).as('row_id') - count = arel_table.project("COUNT(1)").as('ct') + count = arel_table.where(arel_table[column_sym].gteq(zero_interval)).project("COUNT(1)").as('ct') [column_row, row_id, count] end diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb index cf0157269a8..cc9503fb6de 100644 --- a/lib/gitlab/gitaly_client.rb +++ b/lib/gitlab/gitaly_client.rb @@ -388,20 +388,21 @@ module Gitlab end def self.can_use_disk?(storage) - cached_value = MUTEX.synchronize do - @can_use_disk ||= {} - @can_use_disk[storage] - end + false + # cached_value = MUTEX.synchronize do + # @can_use_disk ||= {} + # @can_use_disk[storage] + # end - return cached_value unless cached_value.nil? + # return cached_value unless cached_value.nil? - gitaly_filesystem_id = filesystem_id(storage) - direct_filesystem_id = filesystem_id_from_disk(storage) + # gitaly_filesystem_id = filesystem_id(storage) + # direct_filesystem_id = filesystem_id_from_disk(storage) - MUTEX.synchronize do - @can_use_disk[storage] = gitaly_filesystem_id.present? && - gitaly_filesystem_id == direct_filesystem_id - end + # MUTEX.synchronize do + # @can_use_disk[storage] = gitaly_filesystem_id.present? && + # gitaly_filesystem_id == direct_filesystem_id + # end end def self.filesystem_id(storage) diff --git a/lib/gitlab/graphql.rb b/lib/gitlab/graphql.rb index 8a59e83974f..74c04e5380e 100644 --- a/lib/gitlab/graphql.rb +++ b/lib/gitlab/graphql.rb @@ -3,9 +3,5 @@ module Gitlab module Graphql StandardGraphqlError = Class.new(StandardError) - - def self.enabled? - Feature.enabled?(:graphql, default_enabled: true) - end end end diff --git a/lib/peek/views/redis.rb b/lib/peek/views/redis.rb index ad3c3c9fe01..73de8672fa4 100644 --- a/lib/peek/views/redis.rb +++ b/lib/peek/views/redis.rb @@ -37,6 +37,8 @@ end module Peek module Views module RedisDetailed + REDACTED_MARKER = "<redacted>" + def results super.merge(details: details) end @@ -57,10 +59,12 @@ module Peek end def format_command(cmd) + if cmd.length >= 2 && cmd.first =~ /^auth$/i + cmd[-1] = REDACTED_MARKER # Scrub out the value of the SET calls to avoid binary # data or large data from spilling into the view - if cmd.length >= 2 && cmd.first =~ /set/i - cmd[-1] = "<redacted>" + elsif cmd.length >= 3 && cmd.first =~ /set/i + cmd[2..-1] = REDACTED_MARKER end cmd.join(' ') diff --git a/locale/gitlab.pot b/locale/gitlab.pot index f1cfb0ac06d..9b6e8d8c8a4 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -124,9 +124,6 @@ msgstr "" msgid "%{commit_author_link} authored %{commit_timeago}" msgstr "" -msgid "%{counter_repositories} repositories, %{counter_wikis} wikis, %{counter_build_artifacts} build artifacts, %{counter_lfs_objects} LFS" -msgstr "" - msgid "%{count} more" msgstr "" @@ -8877,6 +8874,9 @@ msgstr "" msgid "Repository storage" msgstr "" +msgid "Repository: %{counter_repositories} / Wikis: %{counter_wikis} / Build Artifacts: %{counter_build_artifacts} / LFS: %{counter_lfs_objects}" +msgstr "" + msgid "Request Access" msgstr "" diff --git a/package.json b/package.json index 5b255f55e33..5955790488a 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "@babel/preset-env": "^7.4.4", "@gitlab/csslab": "^1.9.0", "@gitlab/svgs": "^1.67.0", - "@gitlab/ui": "^5.3.2", + "@gitlab/ui": "^5.5.0", "apollo-cache-inmemory": "^1.5.1", "apollo-client": "^2.5.1", "apollo-link": "^1.2.11", diff --git a/qa/.rspec_parallel b/qa/.rspec_parallel new file mode 100644 index 00000000000..e5927927eaa --- /dev/null +++ b/qa/.rspec_parallel @@ -0,0 +1,5 @@ +--color +--format documentation +--format ParallelTests::RSpec::SummaryLogger --out tmp/spec_summary.log +--format ParallelTests::RSpec::RuntimeLogger --out tmp/parallel_runtime_rspec.log +--require spec_helper diff --git a/qa/Gemfile b/qa/Gemfile index 12994b85322..c46be8a0362 100644 --- a/qa/Gemfile +++ b/qa/Gemfile @@ -1,5 +1,6 @@ source 'https://rubygems.org' +gem 'gitlab-qa' gem 'pry-byebug', '~> 3.5.1', platform: :mri gem 'capybara', '~> 2.16.1' gem 'capybara-screenshot', '~> 1.0.18' @@ -11,3 +12,4 @@ gem 'nokogiri', '~> 1.10.3' gem 'rspec-retry', '~> 0.6.1' gem 'faker', '~> 1.6', '>= 1.6.6' gem 'knapsack', '~> 1.17' +gem 'parallel_tests', '~> 2.29' diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock index 6b0635ed0e2..73aabf2c6ad 100644 --- a/qa/Gemfile.lock +++ b/qa/Gemfile.lock @@ -35,6 +35,7 @@ GEM faker (1.9.3) i18n (>= 0.7) ffi (1.9.25) + gitlab-qa (4.0.0) http-cookie (1.0.3) domain_name (~> 0.5) i18n (0.9.1) @@ -53,6 +54,9 @@ GEM netrc (0.11.0) nokogiri (1.10.3) mini_portile2 (~> 2.4.0) + parallel (1.17.0) + parallel_tests (2.29.0) + parallel pry (0.11.3) coderay (~> 1.1.0) method_source (~> 0.9.0) @@ -104,8 +108,10 @@ DEPENDENCIES capybara (~> 2.16.1) capybara-screenshot (~> 1.0.18) faker (~> 1.6, >= 1.6.6) + gitlab-qa knapsack (~> 1.17) nokogiri (~> 1.10.3) + parallel_tests (~> 2.29) pry-byebug (~> 3.5.1) rake (~> 12.3.0) rspec (~> 3.7) @@ -360,6 +360,7 @@ module QA module Specs autoload :Config, 'qa/specs/config' autoload :Runner, 'qa/specs/runner' + autoload :ParallelRunner, 'qa/specs/parallel_runner' module Helpers autoload :Quarantine, 'qa/specs/helpers/quarantine' diff --git a/qa/qa/runtime/browser.rb b/qa/qa/runtime/browser.rb index ed0779b93cc..2987bb1a213 100644 --- a/qa/qa/runtime/browser.rb +++ b/qa/qa/runtime/browser.rb @@ -13,6 +13,8 @@ module QA NotRespondingError = Class.new(RuntimeError) + CAPYBARA_MAX_WAIT_TIME = 10 + def initialize self.class.configure! end @@ -43,6 +45,8 @@ module QA end end + Capybara.server_port = 9887 + ENV['TEST_ENV_NUMBER'].to_i + return if Capybara.drivers.include?(:chrome) Capybara.register_driver QA::Runtime::Env.browser do |app| @@ -119,7 +123,7 @@ module QA Capybara.configure do |config| config.default_driver = QA::Runtime::Env.browser config.javascript_driver = QA::Runtime::Env.browser - config.default_max_wait_time = 10 + config.default_max_wait_time = CAPYBARA_MAX_WAIT_TIME # https://github.com/mattheworiordan/capybara-screenshot/issues/164 config.save_path = ::File.expand_path('../../tmp', __dir__) end diff --git a/qa/qa/runtime/env.rb b/qa/qa/runtime/env.rb index 96f337dc081..d50f618ff82 100644 --- a/qa/qa/runtime/env.rb +++ b/qa/qa/runtime/env.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require 'gitlab/qa' + module QA module Runtime module Env @@ -7,6 +9,8 @@ module QA attr_writer :personal_access_token, :ldap_username, :ldap_password + ENV_VARIABLES = Gitlab::QA::Runtime::Env::ENV_VARIABLES + # The environment variables used to indicate if the environment under test # supports the given feature SUPPORTED_FEATURES = { @@ -201,6 +205,10 @@ module QA enabled?(ENV[SUPPORTED_FEATURES[feature]], default: true) end + def runtime_scenario_attributes + ENV['QA_RUNTIME_SCENARIO_ATTRIBUTES'] + end + private def remote_grid_credentials diff --git a/qa/qa/runtime/scenario.rb b/qa/qa/runtime/scenario.rb index 5067322804b..3662ebe671b 100644 --- a/qa/qa/runtime/scenario.rb +++ b/qa/qa/runtime/scenario.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require 'json' + module QA module Runtime ## @@ -24,6 +26,10 @@ module QA end end + def from_env(var) + JSON.parse(Runtime::Env.runtime_scenario_attributes).each { |k, v| define(k, v) } + end + def method_missing(name, *) raise ArgumentError, "Scenario attribute `#{name}` not defined!" end diff --git a/qa/qa/scenario/shared_attributes.rb b/qa/qa/scenario/shared_attributes.rb index 40d5c6b1ff1..52f50ec8c27 100644 --- a/qa/qa/scenario/shared_attributes.rb +++ b/qa/qa/scenario/shared_attributes.rb @@ -7,6 +7,7 @@ module QA attribute :gitlab_address, '--address URL', 'Address of the instance to test' attribute :enable_feature, '--enable-feature FEATURE_FLAG', 'Enable a feature before running tests' + attribute :parallel, '--parallel', 'Execute tests in parallel' end end end diff --git a/qa/qa/specs/parallel_runner.rb b/qa/qa/specs/parallel_runner.rb new file mode 100644 index 00000000000..b92fdb610b6 --- /dev/null +++ b/qa/qa/specs/parallel_runner.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require 'open3' + +module QA + module Specs + module ParallelRunner + module_function + + def run(args) + unless args.include?('--') + index = args.index { |opt| opt.include?('features') } + + args.insert(index, '--') if index + end + + env = {} + Runtime::Env::ENV_VARIABLES.each_key do |key| + env[key] = ENV[key] if ENV[key] + end + env['QA_RUNTIME_SCENARIO_ATTRIBUTES'] = Runtime::Scenario.attributes.to_json + env['GITLAB_QA_ACCESS_TOKEN'] = Runtime::API::Client.new(:gitlab).personal_access_token unless env['GITLAB_QA_ACCESS_TOKEN'] + + cmd = "bundle exec parallel_test -t rspec --combine-stderr --serialize-stdout -- #{args.flatten.join(' ')}" + ::Open3.popen2e(env, cmd) do |_, out, wait| + out.each { |line| puts line } + + exit wait.value.exitstatus + end + end + end + end +end diff --git a/qa/qa/specs/runner.rb b/qa/qa/specs/runner.rb index f1cb9378de8..6aa08cf77b4 100644 --- a/qa/qa/specs/runner.rb +++ b/qa/qa/specs/runner.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true +require 'knapsack' require 'rspec/core' require 'rspec/expectations' -require 'knapsack' module QA module Specs @@ -17,44 +17,56 @@ module QA @options = [] end - def perform - args = [] - args.push('--tty') if tty + def paths_from_knapsack + allocator = Knapsack::AllocatorBuilder.new(Knapsack::Adapters::RSpecAdapter).allocator + + QA::Runtime::Logger.info '' + QA::Runtime::Logger.info 'Report specs:' + QA::Runtime::Logger.info allocator.report_node_tests.join(', ') + QA::Runtime::Logger.info '' + QA::Runtime::Logger.info 'Leftover specs:' + QA::Runtime::Logger.info allocator.leftover_node_tests.join(', ') + QA::Runtime::Logger.info '' + + ['--', allocator.node_tests] + end + + def rspec_tags + tags_for_rspec = [] if tags.any? - tags.each { |tag| args.push(['--tag', tag.to_s]) } + tags.each { |tag| tags_for_rspec.push(['--tag', tag.to_s]) } else - args.push(%w[--tag ~orchestrated]) unless (%w[-t --tag] & options).any? + tags_for_rspec.push(%w[--tag ~orchestrated]) unless (%w[-t --tag] & options).any? end - args.push(%w[--tag ~skip_signup_disabled]) if QA::Runtime::Env.signup_disabled? + tags_for_rspec.push(%w[--tag ~skip_signup_disabled]) if QA::Runtime::Env.signup_disabled? QA::Runtime::Env.supported_features.each_key do |key| - args.push(["--tag", "~requires_#{key}"]) unless QA::Runtime::Env.can_test? key + tags_for_rspec.push(%W[--tag ~requires_#{key}]) unless QA::Runtime::Env.can_test? key end - args.push(options) + tags_for_rspec + end - Runtime::Browser.configure! + def perform + args = [] + args.push('--tty') if tty + args.push(rspec_tags) + args.push(options) if Runtime::Env.knapsack? - allocator = Knapsack::AllocatorBuilder.new(Knapsack::Adapters::RSpecAdapter).allocator - - QA::Runtime::Logger.info '' - QA::Runtime::Logger.info 'Report specs:' - QA::Runtime::Logger.info allocator.report_node_tests.join(', ') - QA::Runtime::Logger.info '' - QA::Runtime::Logger.info 'Leftover specs:' - QA::Runtime::Logger.info allocator.leftover_node_tests.join(', ') - QA::Runtime::Logger.info '' - - args.push(['--', allocator.node_tests]) + args.push(paths_from_knapsack) else args.push(DEFAULT_TEST_PATH_ARGS) unless options.any? { |opt| opt =~ %r{/features/} } end - RSpec::Core::Runner.run(args.flatten, $stderr, $stdout).tap do |status| - abort if status.nonzero? + if Runtime::Scenario.attributes[:parallel] + ParallelRunner.run(args.flatten) + else + RSpec::Core::Runner.run(args.flatten, $stderr, $stdout).tap do |status| + abort if status.nonzero? + end end end end diff --git a/qa/spec/page/logging_spec.rb b/qa/spec/page/logging_spec.rb index 0f1ed039149..92a4f7b40e6 100644 --- a/qa/spec/page/logging_spec.rb +++ b/qa/spec/page/logging_spec.rb @@ -91,26 +91,26 @@ describe QA::Support::Page::Logging do it 'logs has_element?' do expect { subject.has_element?(:element) } - .to output(/has_element\? :element \(wait: 2\) returned: true/).to_stdout_from_any_process + .to output(/has_element\? :element \(wait: #{QA::Runtime::Browser::CAPYBARA_MAX_WAIT_TIME}\) returned: true/).to_stdout_from_any_process end it 'logs has_element? with text' do expect { subject.has_element?(:element, text: "some text") } - .to output(/has_element\? :element with text \"some text\" \(wait: 2\) returned: true/).to_stdout_from_any_process + .to output(/has_element\? :element with text \"some text\" \(wait: #{QA::Runtime::Browser::CAPYBARA_MAX_WAIT_TIME}\) returned: true/).to_stdout_from_any_process end it 'logs has_no_element?' do allow(page).to receive(:has_no_css?).and_return(true) expect { subject.has_no_element?(:element) } - .to output(/has_no_element\? :element \(wait: 2\) returned: true/).to_stdout_from_any_process + .to output(/has_no_element\? :element \(wait: #{QA::Runtime::Browser::CAPYBARA_MAX_WAIT_TIME}\) returned: true/).to_stdout_from_any_process end it 'logs has_no_element? with text' do allow(page).to receive(:has_no_css?).and_return(true) expect { subject.has_no_element?(:element, text: "more text") } - .to output(/has_no_element\? :element with text \"more text\" \(wait: 2\) returned: true/).to_stdout_from_any_process + .to output(/has_no_element\? :element with text \"more text\" \(wait: #{QA::Runtime::Browser::CAPYBARA_MAX_WAIT_TIME}\) returned: true/).to_stdout_from_any_process end it 'logs has_text?' do diff --git a/qa/spec/spec_helper.rb b/qa/spec/spec_helper.rb index f25dbf3a8ab..21bfd2876a9 100644 --- a/qa/spec/spec_helper.rb +++ b/qa/spec/spec_helper.rb @@ -8,6 +8,10 @@ if ENV['CI'] && QA::Runtime::Env.knapsack? && !ENV['NO_KNAPSACK'] Knapsack::Adapters::RSpecAdapter.bind end +QA::Runtime::Browser.configure! + +QA::Runtime::Scenario.from_env(QA::Runtime::Env.runtime_scenario_attributes) if QA::Runtime::Env.runtime_scenario_attributes + %w[helpers shared_examples].each do |d| Dir[::File.join(__dir__, d, '**', '*.rb')].each { |f| require f } end diff --git a/qa/spec/specs/parallel_runner_spec.rb b/qa/spec/specs/parallel_runner_spec.rb new file mode 100644 index 00000000000..67d94a1f648 --- /dev/null +++ b/qa/spec/specs/parallel_runner_spec.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +describe QA::Specs::ParallelRunner do + include Helpers::StubENV + + before do + allow(QA::Runtime::Scenario).to receive(:attributes).and_return(parallel: true) + stub_env('GITLAB_QA_ACCESS_TOKEN', 'skip_token_creation') + end + + it 'passes args to parallel_tests' do + expect_cli_arguments(['--tag', '~orchestrated', *QA::Specs::Runner::DEFAULT_TEST_PATH_ARGS]) + + subject.run(['--tag', '~orchestrated', *QA::Specs::Runner::DEFAULT_TEST_PATH_ARGS]) + end + + it 'passes a given test path to parallel_tests and adds a separator' do + expect_cli_arguments(%w[-- qa/specs/features/foo]) + + subject.run(%w[qa/specs/features/foo]) + end + + it 'passes tags and test paths to parallel_tests and adds a separator' do + expect_cli_arguments(%w[--tag smoke -- qa/specs/features/foo qa/specs/features/bar]) + + subject.run(%w[--tag smoke qa/specs/features/foo qa/specs/features/bar]) + end + + it 'passes tags and test paths with separators to parallel_tests' do + expect_cli_arguments(%w[-- --tag smoke -- qa/specs/features/foo qa/specs/features/bar]) + + subject.run(%w[-- --tag smoke -- qa/specs/features/foo qa/specs/features/bar]) + end + + it 'passes supported environment variables' do + # Test only env vars starting with GITLAB because some of the others + # affect how the runner behaves, and we're not concerned with those + # behaviors in this test + gitlab_env_vars = QA::Runtime::Env::ENV_VARIABLES.reject { |v| !v.start_with?('GITLAB') } + + gitlab_env_vars.each do |k, v| + stub_env(k, v) + end + + gitlab_env_vars['QA_RUNTIME_SCENARIO_ATTRIBUTES'] = '{"parallel":true}' + + expect_cli_arguments([], gitlab_env_vars) + + subject.run([]) + end + + def expect_cli_arguments(arguments, env = { 'QA_RUNTIME_SCENARIO_ATTRIBUTES' => '{"parallel":true}' }) + cmd = "bundle exec parallel_test -t rspec --combine-stderr --serialize-stdout -- #{arguments.join(' ')}" + expect(Open3).to receive(:popen2e) + .with(hash_including(env), cmd) + .and_return(0) + end +end diff --git a/qa/spec/specs/runner_spec.rb b/qa/spec/specs/runner_spec.rb index f94145d148e..6c533c6dc7d 100644 --- a/qa/spec/specs/runner_spec.rb +++ b/qa/spec/specs/runner_spec.rb @@ -58,11 +58,11 @@ describe QA::Specs::Runner do end end - context 'when "-- qa/specs/features/foo" is set as options' do - subject { described_class.new.tap { |runner| runner.options = %w[-- qa/specs/features/foo] } } + context 'when "--tag smoke" and "qa/specs/features/foo" are set as options' do + subject { described_class.new.tap { |runner| runner.options = %w[--tag smoke qa/specs/features/foo] } } - it 'passes the given tests path and excludes the orchestrated tag' do - expect_rspec_runner_arguments(['--tag', '~orchestrated', '--', 'qa/specs/features/foo']) + it 'focuses on the given tag and includes the path without excluding the orchestrated tag' do + expect_rspec_runner_arguments(['--tag', 'smoke', 'qa/specs/features/foo']) subject.perform end diff --git a/spec/features/admin/admin_sees_project_statistics_spec.rb b/spec/features/admin/admin_sees_project_statistics_spec.rb index b5323a1c76d..ecd0aab925b 100644 --- a/spec/features/admin/admin_sees_project_statistics_spec.rb +++ b/spec/features/admin/admin_sees_project_statistics_spec.rb @@ -15,7 +15,7 @@ describe "Admin > Admin sees project statistics" do let(:project) { create(:project, :repository) } it "shows project statistics" do - expect(page).to have_content("Storage: 0 Bytes (0 Bytes repositories, 0 Bytes wikis, 0 Bytes build artifacts, 0 Bytes LFS)") + expect(page).to have_content("Storage: 0 Bytes (Repository: 0 Bytes / Wikis: 0 Bytes / Build Artifacts: 0 Bytes / LFS: 0 Bytes)") end end diff --git a/spec/frontend/confidential_merge_request/components/__snapshots__/project_form_group_spec.js.snap b/spec/frontend/confidential_merge_request/components/__snapshots__/project_form_group_spec.js.snap index ba0ee8dfd59..fd307ce5ab3 100644 --- a/spec/frontend/confidential_merge_request/components/__snapshots__/project_form_group_spec.js.snap +++ b/spec/frontend/confidential_merge_request/components/__snapshots__/project_form_group_spec.js.snap @@ -28,23 +28,6 @@ exports[`Confidential merge request project form group component renders empty s </a> and set the forks visiblity to private. </span> - - <gllink-stub - class="help-link" - href="/help" - target="_blank" - > - <span - class="sr-only" - > - Read more - </span> - - <i - aria-hidden="true" - class="fa fa-question-circle" - /> - </gllink-stub> </p> </div> </div> @@ -78,23 +61,6 @@ exports[`Confidential merge request project form group component renders fork dr </a> and set the forks visiblity to private. </span> - - <gllink-stub - class="help-link" - href="/help" - target="_blank" - > - <span - class="sr-only" - > - Read more - </span> - - <i - aria-hidden="true" - class="fa fa-question-circle" - /> - </gllink-stub> </p> </div> </div> diff --git a/spec/helpers/storage_helper_spec.rb b/spec/helpers/storage_helper_spec.rb index 62c00964524..4bd0fbb76ca 100644 --- a/spec/helpers/storage_helper_spec.rb +++ b/spec/helpers/storage_helper_spec.rb @@ -31,7 +31,7 @@ describe StorageHelper do build_artifacts_size: 30.megabytes)) end - let(:message) { '10 KB repositories, 10 Bytes wikis, 30 MB build artifacts, 20 GB LFS' } + let(:message) { 'Repository: 10 KB / Wikis: 10 Bytes / Build Artifacts: 30 MB / LFS: 20 GB' } it 'works on ProjectStatistics' do expect(helper.storage_counters_details(project.statistics)).to eq(message) diff --git a/spec/javascripts/issuable_spec.js b/spec/javascripts/issuable_spec.js index 25543053eba..4d57bfb1b33 100644 --- a/spec/javascripts/issuable_spec.js +++ b/spec/javascripts/issuable_spec.js @@ -2,14 +2,14 @@ import $ from 'jquery'; import MockAdaptor from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; import IssuableIndex from '~/issuable_index'; +import issuableInitBulkUpdateSidebar from '~/issuable_init_bulk_update_sidebar'; describe('Issuable', () => { - let Issuable; describe('initBulkUpdate', () => { it('should not set bulkUpdateSidebar', () => { - Issuable = new IssuableIndex('issue_'); + new IssuableIndex('issue_'); // eslint-disable-line no-new - expect(Issuable.bulkUpdateSidebar).not.toBeDefined(); + expect(issuableInitBulkUpdateSidebar.bulkUpdateSidebar).toBeNull(); }); it('should set bulkUpdateSidebar', () => { @@ -17,9 +17,9 @@ describe('Issuable', () => { element.classList.add('issues-bulk-update'); document.body.appendChild(element); - Issuable = new IssuableIndex('issue_'); + new IssuableIndex('issue_'); // eslint-disable-line no-new - expect(Issuable.bulkUpdateSidebar).toBeDefined(); + expect(issuableInitBulkUpdateSidebar.bulkUpdateSidebar).toBeDefined(); }); }); @@ -36,7 +36,7 @@ describe('Issuable', () => { input.setAttribute('id', 'issuable_email'); document.body.appendChild(input); - Issuable = new IssuableIndex('issue_'); + new IssuableIndex('issue_'); // eslint-disable-line no-new mock = new MockAdaptor(axios); diff --git a/spec/lib/gitlab/cycle_analytics/test_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/test_stage_spec.rb index eacde22cd56..8633a63849f 100644 --- a/spec/lib/gitlab/cycle_analytics/test_stage_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/test_stage_spec.rb @@ -3,6 +3,40 @@ require 'lib/gitlab/cycle_analytics/shared_stage_spec' describe Gitlab::CycleAnalytics::TestStage do let(:stage_name) { :test } + let(:project) { create(:project) } + let(:stage) { described_class.new(project: project, options: { from: 2.days.ago, current_user: project.creator }) } it_behaves_like 'base stage' + + describe '#median' do + before do + issue_1 = create(:issue, project: project, created_at: 90.minutes.ago) + issue_2 = create(:issue, project: project, created_at: 60.minutes.ago) + issue_3 = create(:issue, project: project, created_at: 60.minutes.ago) + mr_1 = create(:merge_request, :closed, source_project: project, created_at: 60.minutes.ago) + mr_2 = create(:merge_request, :closed, source_project: project, created_at: 40.minutes.ago, source_branch: 'A') + mr_3 = create(:merge_request, source_project: project, created_at: 10.minutes.ago, source_branch: 'B') + mr_4 = create(:merge_request, source_project: project, created_at: 10.minutes.ago, source_branch: 'C') + mr_5 = create(:merge_request, source_project: project, created_at: 10.minutes.ago, source_branch: 'D') + mr_1.metrics.update!(latest_build_started_at: 32.minutes.ago, latest_build_finished_at: 2.minutes.ago) + mr_2.metrics.update!(latest_build_started_at: 62.minutes.ago, latest_build_finished_at: 32.minutes.ago) + mr_3.metrics.update!(latest_build_started_at: nil, latest_build_finished_at: nil) + mr_4.metrics.update!(latest_build_started_at: nil, latest_build_finished_at: nil) + mr_5.metrics.update!(latest_build_started_at: nil, latest_build_finished_at: nil) + + create(:merge_requests_closing_issues, merge_request: mr_1, issue: issue_1) + create(:merge_requests_closing_issues, merge_request: mr_2, issue: issue_2) + create(:merge_requests_closing_issues, merge_request: mr_3, issue: issue_3) + create(:merge_requests_closing_issues, merge_request: mr_4, issue: issue_3) + create(:merge_requests_closing_issues, merge_request: mr_5, issue: issue_3) + end + + around do |example| + Timecop.freeze { example.run } + end + + it 'counts median from issues with metrics' do + expect(stage.median).to eq(ISSUES_MEDIAN) + end + end end diff --git a/spec/lib/gitlab/git/rugged_impl/use_rugged_spec.rb b/spec/lib/gitlab/git/rugged_impl/use_rugged_spec.rb index f957ed00945..e7ef9d08f80 100644 --- a/spec/lib/gitlab/git/rugged_impl/use_rugged_spec.rb +++ b/spec/lib/gitlab/git/rugged_impl/use_rugged_spec.rb @@ -30,6 +30,7 @@ describe Gitlab::Git::RuggedImpl::UseRugged, :seed_helper do end it 'returns true when gitaly matches disk' do + pending('temporary disabled because of https://gitlab.com/gitlab-org/gitlab-ce/issues/64338') expect(subject.use_rugged?(repository, feature_flag_name)).to be true end @@ -48,6 +49,7 @@ describe Gitlab::Git::RuggedImpl::UseRugged, :seed_helper do end it "doesn't lead to a second rpc call because gitaly client should use the cached value" do + pending('temporary disabled because of https://gitlab.com/gitlab-org/gitlab-ce/issues/64338') expect(subject.use_rugged?(repository, feature_flag_name)).to be true expect(Gitlab::GitalyClient).not_to receive(:filesystem_id) diff --git a/spec/lib/peek/views/redis_detailed_spec.rb b/spec/lib/peek/views/redis_detailed_spec.rb new file mode 100644 index 00000000000..da13b6df53b --- /dev/null +++ b/spec/lib/peek/views/redis_detailed_spec.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Peek::Views::RedisDetailed do + let(:redis_detailed_class) do + Class.new do + include Peek::Views::RedisDetailed + end + end + + subject { redis_detailed_class.new } + + using RSpec::Parameterized::TableSyntax + + where(:cmd, :expected) do + [:auth, 'test'] | 'auth <redacted>' + [:set, 'key', 'value'] | 'set key <redacted>' + [:set, 'bad'] | 'set bad' + [:hmset, 'key1', 'value1', 'key2', 'value2'] | 'hmset key1 <redacted>' + [:get, 'key'] | 'get key' + end + + with_them do + it 'scrubs Redis commands', :request_store do + subject.detail_store << { cmd: cmd, duration: 1.second } + + expect(subject.details.count).to eq(1) + expect(subject.details.first) + .to eq({ + cmd: expected, + duration: 1000 + }) + end + end +end diff --git a/spec/models/concerns/project_api_compatibility_spec.rb b/spec/models/concerns/project_api_compatibility_spec.rb new file mode 100644 index 00000000000..8cecd4fe7bc --- /dev/null +++ b/spec/models/concerns/project_api_compatibility_spec.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe ProjectAPICompatibility do + let(:project) { create(:project) } + + # git_strategy + it "converts build_git_strategy=fetch to build_allow_git_fetch=true" do + project.update!(build_git_strategy: 'fetch') + expect(project.build_allow_git_fetch).to eq(true) + end + + it "converts build_git_strategy=clone to build_allow_git_fetch=false" do + project.update!(build_git_strategy: 'clone') + expect(project.build_allow_git_fetch).to eq(false) + end + + # auto_devops_enabled + it "converts auto_devops_enabled=false to auto_devops_enabled?=false" do + expect(project.auto_devops_enabled?).to eq(true) + project.update!(auto_devops_enabled: false) + expect(project.auto_devops_enabled?).to eq(false) + end + + it "converts auto_devops_enabled=true to auto_devops_enabled?=true" do + expect(project.auto_devops_enabled?).to eq(true) + project.update!(auto_devops_enabled: true) + expect(project.auto_devops_enabled?).to eq(true) + end + + # auto_devops_deploy_strategy + it "converts auto_devops_deploy_strategy=timed_incremental to auto_devops.deploy_strategy=timed_incremental" do + expect(project.auto_devops).to be_nil + project.update!(auto_devops_deploy_strategy: 'timed_incremental') + expect(project.auto_devops.deploy_strategy).to eq('timed_incremental') + end +end diff --git a/spec/models/concerns/project_features_compatibility_spec.rb b/spec/models/concerns/project_features_compatibility_spec.rb index 5aa43b58217..1fe176ab5af 100644 --- a/spec/models/concerns/project_features_compatibility_spec.rb +++ b/spec/models/concerns/project_features_compatibility_spec.rb @@ -4,7 +4,8 @@ require 'spec_helper' describe ProjectFeaturesCompatibility do let(:project) { create(:project) } - let(:features) { %w(issues wiki builds merge_requests snippets) } + let(:features_except_repository) { %w(issues wiki builds merge_requests snippets) } + let(:features) { features_except_repository + ['repository'] } # We had issues_enabled, snippets_enabled, builds_enabled, merge_requests_enabled and issues_enabled fields on projects table # All those fields got moved to a new table called project_feature and are now integers instead of booleans @@ -12,30 +13,37 @@ describe ProjectFeaturesCompatibility do # So we can keep it compatible it "converts fields from 'true' to ProjectFeature::ENABLED" do - features.each do |feature| + features_except_repository.each do |feature| project.update_attribute("#{feature}_enabled".to_sym, "true") expect(project.project_feature.public_send("#{feature}_access_level")).to eq(ProjectFeature::ENABLED) end end it "converts fields from 'false' to ProjectFeature::DISABLED" do - features.each do |feature| + features_except_repository.each do |feature| project.update_attribute("#{feature}_enabled".to_sym, "false") expect(project.project_feature.public_send("#{feature}_access_level")).to eq(ProjectFeature::DISABLED) end end it "converts fields from true to ProjectFeature::ENABLED" do - features.each do |feature| + features_except_repository.each do |feature| project.update_attribute("#{feature}_enabled".to_sym, true) expect(project.project_feature.public_send("#{feature}_access_level")).to eq(ProjectFeature::ENABLED) end end it "converts fields from false to ProjectFeature::DISABLED" do - features.each do |feature| + features_except_repository.each do |feature| project.update_attribute("#{feature}_enabled".to_sym, false) expect(project.project_feature.public_send("#{feature}_access_level")).to eq(ProjectFeature::DISABLED) end end + + it "accepts private as ProjectFeature::PRIVATE" do + features.each do |feature| + project.update!("#{feature}_access_level".to_sym => 'private') + expect(project.project_feature.public_send("#{feature}_access_level")).to eq(ProjectFeature::PRIVATE) + end + end end diff --git a/spec/requests/api/graphql_spec.rb b/spec/requests/api/graphql_spec.rb index 67371cb35b6..54401ec4085 100644 --- a/spec/requests/api/graphql_spec.rb +++ b/spec/requests/api/graphql_spec.rb @@ -6,16 +6,6 @@ describe 'GraphQL' do let(:query) { graphql_query_for('echo', 'text' => 'Hello world' ) } - context 'graphql is disabled by feature flag' do - before do - stub_feature_flags(graphql: false) - end - - it 'does not generate a route for GraphQL' do - expect { post_graphql(query) }.to raise_error(ActionController::RoutingError) - end - end - context 'logging' do shared_examples 'logging a graphql query' do let(:expected_params) do diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index c67412a44c1..a2aae257352 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -1102,6 +1102,12 @@ describe API::Projects do expect(json_response['wiki_enabled']).to be_present expect(json_response['jobs_enabled']).to be_present expect(json_response['snippets_enabled']).to be_present + expect(json_response['snippets_access_level']).to be_present + expect(json_response['repository_access_level']).to be_present + expect(json_response['issues_access_level']).to be_present + expect(json_response['merge_requests_access_level']).to be_present + expect(json_response['wiki_access_level']).to be_present + expect(json_response['builds_access_level']).to be_present expect(json_response['resolve_outdated_diff_discussions']).to eq(project.resolve_outdated_diff_discussions) expect(json_response['container_registry_enabled']).to be_present expect(json_response['created_at']).to be_present @@ -1913,6 +1919,34 @@ describe API::Projects do end end + it 'updates builds_access_level' do + project_param = { builds_access_level: 'private' } + + put api("/projects/#{project3.id}", user), params: project_param + + expect(response).to have_gitlab_http_status(200) + + expect(json_response['builds_access_level']).to eq('private') + end + + it 'updates build_git_strategy' do + project_param = { build_git_strategy: 'clone' } + + put api("/projects/#{project3.id}", user), params: project_param + + expect(response).to have_gitlab_http_status(200) + + expect(json_response['build_git_strategy']).to eq('clone') + end + + it 'rejects to update build_git_strategy when build_git_strategy is invalid' do + project_param = { build_git_strategy: 'invalid' } + + put api("/projects/#{project3.id}", user), params: project_param + + expect(response).to have_gitlab_http_status(400) + end + it 'updates merge_method' do project_param = { merge_method: 'ff' } @@ -1946,6 +1980,26 @@ describe API::Projects do '-/system/project/avatar/'\ "#{project3.id}/banana_sample.gif") end + + it 'updates auto_devops_deploy_strategy' do + project_param = { auto_devops_deploy_strategy: 'timed_incremental' } + + put api("/projects/#{project3.id}", user), params: project_param + + expect(response).to have_gitlab_http_status(200) + + expect(json_response['auto_devops_deploy_strategy']).to eq('timed_incremental') + end + + it 'updates auto_devops_enabled' do + project_param = { auto_devops_enabled: false } + + put api("/projects/#{project3.id}", user), params: project_param + + expect(response).to have_gitlab_http_status(200) + + expect(json_response['auto_devops_enabled']).to eq(false) + end end context 'when authenticated as project maintainer' do diff --git a/spec/routing/api_routing_spec.rb b/spec/routing/api_routing_spec.rb deleted file mode 100644 index 3c48ead4ff2..00000000000 --- a/spec/routing/api_routing_spec.rb +++ /dev/null @@ -1,23 +0,0 @@ -require 'spec_helper' - -describe 'api', 'routing' do - context 'when graphql is disabled' do - before do - stub_feature_flags(graphql: false) - end - - it 'does not route to the GraphqlController' do - expect(post('/api/graphql')).not_to route_to('graphql#execute') - end - end - - context 'when graphql is enabled' do - before do - stub_feature_flags(graphql: true) - end - - it 'routes to the GraphqlController' do - expect(post('/api/graphql')).to route_to('graphql#execute') - end - end -end diff --git a/spec/services/issuable/bulk_update_service_spec.rb b/spec/services/issuable/bulk_update_service_spec.rb index aebc5ba2874..3d2d4b5f216 100644 --- a/spec/services/issuable/bulk_update_service_spec.rb +++ b/spec/services/issuable/bulk_update_service_spec.rb @@ -11,343 +11,371 @@ describe Issuable::BulkUpdateService do .reverse_merge(issuable_ids: Array(issuables).map(&:id).join(',')) type = Array(issuables).first.model_name.param_key - Issuable::BulkUpdateService.new(project, user, bulk_update_params).execute(type) + Issuable::BulkUpdateService.new(user, bulk_update_params).execute(type) end - describe 'close issues' do - let(:issues) { create_list(:issue, 2, project: project) } - - it 'succeeds and returns the correct number of issues updated' do - result = bulk_update(issues, state_event: 'close') + shared_examples 'updates milestones' do + it 'succeeds' do + result = bulk_update(issues, milestone_id: milestone.id) expect(result[:success]).to be_truthy expect(result[:count]).to eq(issues.count) end - it 'closes all the issues passed' do - bulk_update(issues, state_event: 'close') + it 'updates the issues milestone' do + bulk_update(issues, milestone_id: milestone.id) - expect(project.issues.opened).to be_empty - expect(project.issues.closed).not_to be_empty + issues.each do |issue| + expect(issue.reload.milestone).to eq(milestone) + end end + end - context 'when issue for a different project is created' do - let(:private_project) { create(:project, :private) } - let(:issue) { create(:issue, project: private_project, author: user) } + context 'with project issues' do + describe 'close issues' do + let(:issues) { create_list(:issue, 2, project: project) } - context 'when user has access to the project' do - it 'closes all issues passed' do - private_project.add_maintainer(user) + it 'succeeds and returns the correct number of issues updated' do + result = bulk_update(issues, state_event: 'close') - bulk_update(issues + [issue], state_event: 'close') + expect(result[:success]).to be_truthy + expect(result[:count]).to eq(issues.count) + end - expect(project.issues.opened).to be_empty - expect(project.issues.closed).not_to be_empty - expect(private_project.issues.closed).not_to be_empty - end + it 'closes all the issues passed' do + bulk_update(issues, state_event: 'close') + + expect(project.issues.opened).to be_empty + expect(project.issues.closed).not_to be_empty end - context 'when user does not have access to project' do - it 'only closes all issues that the user has access to' do - bulk_update(issues + [issue], state_event: 'close') + context 'when issue for a different project is created' do + let(:private_project) { create(:project, :private) } + let(:issue) { create(:issue, project: private_project, author: user) } + + context 'when user has access to the project' do + it 'closes all issues passed' do + private_project.add_maintainer(user) + + bulk_update(issues + [issue], state_event: 'close') + + expect(project.issues.opened).to be_empty + expect(project.issues.closed).not_to be_empty + expect(private_project.issues.closed).not_to be_empty + end + end + + context 'when user does not have access to project' do + it 'only closes all issues that the user has access to' do + bulk_update(issues + [issue], state_event: 'close') - expect(project.issues.opened).to be_empty - expect(project.issues.closed).not_to be_empty - expect(private_project.issues.closed).to be_empty + expect(project.issues.opened).to be_empty + expect(project.issues.closed).not_to be_empty + expect(private_project.issues.closed).to be_empty + end end end end - end - describe 'reopen issues' do - let(:issues) { create_list(:closed_issue, 2, project: project) } + describe 'reopen issues' do + let(:issues) { create_list(:closed_issue, 2, project: project) } - it 'succeeds and returns the correct number of issues updated' do - result = bulk_update(issues, state_event: 'reopen') + it 'succeeds and returns the correct number of issues updated' do + result = bulk_update(issues, state_event: 'reopen') - expect(result[:success]).to be_truthy - expect(result[:count]).to eq(issues.count) - end + expect(result[:success]).to be_truthy + expect(result[:count]).to eq(issues.count) + end - it 'reopens all the issues passed' do - bulk_update(issues, state_event: 'reopen') + it 'reopens all the issues passed' do + bulk_update(issues, state_event: 'reopen') - expect(project.issues.closed).to be_empty - expect(project.issues.opened).not_to be_empty + expect(project.issues.closed).to be_empty + expect(project.issues.opened).not_to be_empty + end end - end - describe 'updating merge request assignee' do - let(:merge_request) { create(:merge_request, target_project: project, source_project: project, assignees: [user]) } + describe 'updating merge request assignee' do + let(:merge_request) { create(:merge_request, target_project: project, source_project: project, assignees: [user]) } - context 'when the new assignee ID is a valid user' do - it 'succeeds' do - new_assignee = create(:user) - project.add_developer(new_assignee) + context 'when the new assignee ID is a valid user' do + it 'succeeds' do + new_assignee = create(:user) + project.add_developer(new_assignee) - result = bulk_update(merge_request, assignee_ids: [user.id, new_assignee.id]) + result = bulk_update(merge_request, assignee_ids: [user.id, new_assignee.id]) - expect(result[:success]).to be_truthy - expect(result[:count]).to eq(1) - end + expect(result[:success]).to be_truthy + expect(result[:count]).to eq(1) + end - it 'updates the assignee to the user ID passed' do - assignee = create(:user) - project.add_developer(assignee) + it 'updates the assignee to the user ID passed' do + assignee = create(:user) + project.add_developer(assignee) - expect { bulk_update(merge_request, assignee_ids: [assignee.id]) } - .to change { merge_request.reload.assignee_ids }.from([user.id]).to([assignee.id]) + expect { bulk_update(merge_request, assignee_ids: [assignee.id]) } + .to change { merge_request.reload.assignee_ids }.from([user.id]).to([assignee.id]) + end end - end - context "when the new assignee ID is #{IssuableFinder::NONE}" do - it 'unassigns the issues' do - expect { bulk_update(merge_request, assignee_ids: [IssuableFinder::NONE]) } - .to change { merge_request.reload.assignee_ids }.to([]) + context "when the new assignee ID is #{IssuableFinder::NONE}" do + it 'unassigns the issues' do + expect { bulk_update(merge_request, assignee_ids: [IssuableFinder::NONE]) } + .to change { merge_request.reload.assignee_ids }.to([]) + end end - end - context 'when the new assignee ID is not present' do - it 'does not unassign' do - expect { bulk_update(merge_request, assignee_ids: []) } - .not_to change { merge_request.reload.assignee_ids } + context 'when the new assignee ID is not present' do + it 'does not unassign' do + expect { bulk_update(merge_request, assignee_ids: []) } + .not_to change { merge_request.reload.assignee_ids } + end end end - end - describe 'updating issue assignee' do - let(:issue) { create(:issue, project: project, assignees: [user]) } + describe 'updating issue assignee' do + let(:issue) { create(:issue, project: project, assignees: [user]) } - context 'when the new assignee ID is a valid user' do - it 'succeeds' do - new_assignee = create(:user) - project.add_developer(new_assignee) + context 'when the new assignee ID is a valid user' do + it 'succeeds' do + new_assignee = create(:user) + project.add_developer(new_assignee) - result = bulk_update(issue, assignee_ids: [new_assignee.id]) + result = bulk_update(issue, assignee_ids: [new_assignee.id]) - expect(result[:success]).to be_truthy - expect(result[:count]).to eq(1) - end + expect(result[:success]).to be_truthy + expect(result[:count]).to eq(1) + end - it 'updates the assignee to the user ID passed' do - assignee = create(:user) - project.add_developer(assignee) - expect { bulk_update(issue, assignee_ids: [assignee.id]) } - .to change { issue.reload.assignees.first }.from(user).to(assignee) + it 'updates the assignee to the user ID passed' do + assignee = create(:user) + project.add_developer(assignee) + expect { bulk_update(issue, assignee_ids: [assignee.id]) } + .to change { issue.reload.assignees.first }.from(user).to(assignee) + end end - end - context "when the new assignee ID is #{IssuableFinder::NONE}" do - it "unassigns the issues" do - expect { bulk_update(issue, assignee_ids: [IssuableFinder::NONE.to_s]) } - .to change { issue.reload.assignees.count }.from(1).to(0) + context "when the new assignee ID is #{IssuableFinder::NONE}" do + it "unassigns the issues" do + expect { bulk_update(issue, assignee_ids: [IssuableFinder::NONE.to_s]) } + .to change { issue.reload.assignees.count }.from(1).to(0) + end end - end - context 'when the new assignee ID is not present' do - it 'does not unassign' do - expect { bulk_update(issue, assignee_ids: []) } - .not_to change { issue.reload.assignees } + context 'when the new assignee ID is not present' do + it 'does not unassign' do + expect { bulk_update(issue, assignee_ids: []) } + .not_to change { issue.reload.assignees } + end end end - end - - describe 'updating milestones' do - let(:issue) { create(:issue, project: project) } - let(:milestone) { create(:milestone, project: project) } - it 'succeeds' do - result = bulk_update(issue, milestone_id: milestone.id) + describe 'updating milestones' do + let(:issues) { [create(:issue, project: project)] } + let(:milestone) { create(:milestone, project: project) } - expect(result[:success]).to be_truthy - expect(result[:count]).to eq(1) + it_behaves_like 'updates milestones' end - it 'updates the issue milestone' do - expect { bulk_update(issue, milestone_id: milestone.id) } - .to change { issue.reload.milestone }.from(nil).to(milestone) - end - end - - describe 'updating labels' do - def create_issue_with_labels(labels) - create(:labeled_issue, project: project, labels: labels) - end + describe 'updating labels' do + def create_issue_with_labels(labels) + create(:labeled_issue, project: project, labels: labels) + end - let(:bug) { create(:label, project: project) } - let(:regression) { create(:label, project: project) } - let(:merge_requests) { create(:label, project: project) } - - let(:issue_all_labels) { create_issue_with_labels([bug, regression, merge_requests]) } - let(:issue_bug_and_regression) { create_issue_with_labels([bug, regression]) } - let(:issue_bug_and_merge_requests) { create_issue_with_labels([bug, merge_requests]) } - let(:issue_no_labels) { create(:issue, project: project) } - let(:issues) { [issue_all_labels, issue_bug_and_regression, issue_bug_and_merge_requests, issue_no_labels] } - - let(:labels) { [] } - let(:add_labels) { [] } - let(:remove_labels) { [] } - - let(:bulk_update_params) do - { - label_ids: labels.map(&:id), - add_label_ids: add_labels.map(&:id), - remove_label_ids: remove_labels.map(&:id) - } - end + let(:bug) { create(:label, project: project) } + let(:regression) { create(:label, project: project) } + let(:merge_requests) { create(:label, project: project) } - before do - bulk_update(issues, bulk_update_params) - end + let(:issue_all_labels) { create_issue_with_labels([bug, regression, merge_requests]) } + let(:issue_bug_and_regression) { create_issue_with_labels([bug, regression]) } + let(:issue_bug_and_merge_requests) { create_issue_with_labels([bug, merge_requests]) } + let(:issue_no_labels) { create(:issue, project: project) } + let(:issues) { [issue_all_labels, issue_bug_and_regression, issue_bug_and_merge_requests, issue_no_labels] } - context 'when label_ids are passed' do - let(:issues) { [issue_all_labels, issue_no_labels] } - let(:labels) { [bug, regression] } + let(:labels) { [] } + let(:add_labels) { [] } + let(:remove_labels) { [] } - it 'updates the labels of all issues passed to the labels passed' do - expect(issues.map(&:reload).map(&:label_ids)).to all(match_array(labels.map(&:id))) + let(:bulk_update_params) do + { + label_ids: labels.map(&:id), + add_label_ids: add_labels.map(&:id), + remove_label_ids: remove_labels.map(&:id) + } end - it 'does not update issues not passed in' do - expect(issue_bug_and_regression.label_ids).to contain_exactly(bug.id, regression.id) + before do + bulk_update(issues, bulk_update_params) end - context 'when those label IDs are empty' do - let(:labels) { [] } + context 'when label_ids are passed' do + let(:issues) { [issue_all_labels, issue_no_labels] } + let(:labels) { [bug, regression] } - it 'updates the issues passed to have no labels' do - expect(issues.map(&:reload).map(&:label_ids)).to all(be_empty) + it 'updates the labels of all issues passed to the labels passed' do + expect(issues.map(&:reload).map(&:label_ids)).to all(match_array(labels.map(&:id))) end - end - end - context 'when add_label_ids are passed' do - let(:issues) { [issue_all_labels, issue_bug_and_merge_requests, issue_no_labels] } - let(:add_labels) { [bug, regression, merge_requests] } + it 'does not update issues not passed in' do + expect(issue_bug_and_regression.label_ids).to contain_exactly(bug.id, regression.id) + end - it 'adds those label IDs to all issues passed' do - expect(issues.map(&:reload).map(&:label_ids)).to all(include(*add_labels.map(&:id))) - end + context 'when those label IDs are empty' do + let(:labels) { [] } - it 'does not update issues not passed in' do - expect(issue_bug_and_regression.label_ids).to contain_exactly(bug.id, regression.id) + it 'updates the issues passed to have no labels' do + expect(issues.map(&:reload).map(&:label_ids)).to all(be_empty) + end + end end - end - context 'when remove_label_ids are passed' do - let(:issues) { [issue_all_labels, issue_bug_and_merge_requests, issue_no_labels] } - let(:remove_labels) { [bug, regression, merge_requests] } + context 'when add_label_ids are passed' do + let(:issues) { [issue_all_labels, issue_bug_and_merge_requests, issue_no_labels] } + let(:add_labels) { [bug, regression, merge_requests] } - it 'removes those label IDs from all issues passed' do - expect(issues.map(&:reload).map(&:label_ids)).to all(be_empty) - end + it 'adds those label IDs to all issues passed' do + expect(issues.map(&:reload).map(&:label_ids)).to all(include(*add_labels.map(&:id))) + end - it 'does not update issues not passed in' do - expect(issue_bug_and_regression.label_ids).to contain_exactly(bug.id, regression.id) + it 'does not update issues not passed in' do + expect(issue_bug_and_regression.label_ids).to contain_exactly(bug.id, regression.id) + end end - end - context 'when add_label_ids and remove_label_ids are passed' do - let(:issues) { [issue_all_labels, issue_bug_and_merge_requests, issue_no_labels] } - let(:add_labels) { [bug] } - let(:remove_labels) { [merge_requests] } + context 'when remove_label_ids are passed' do + let(:issues) { [issue_all_labels, issue_bug_and_merge_requests, issue_no_labels] } + let(:remove_labels) { [bug, regression, merge_requests] } - it 'adds the label IDs to all issues passed' do - expect(issues.map(&:reload).map(&:label_ids)).to all(include(bug.id)) - end + it 'removes those label IDs from all issues passed' do + expect(issues.map(&:reload).map(&:label_ids)).to all(be_empty) + end - it 'removes the label IDs from all issues passed' do - expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(merge_requests.id) + it 'does not update issues not passed in' do + expect(issue_bug_and_regression.label_ids).to contain_exactly(bug.id, regression.id) + end end - it 'does not update issues not passed in' do - expect(issue_bug_and_regression.label_ids).to contain_exactly(bug.id, regression.id) - end - end + context 'when add_label_ids and remove_label_ids are passed' do + let(:issues) { [issue_all_labels, issue_bug_and_merge_requests, issue_no_labels] } + let(:add_labels) { [bug] } + let(:remove_labels) { [merge_requests] } - context 'when add_label_ids and label_ids are passed' do - let(:issues) { [issue_all_labels, issue_bug_and_regression, issue_bug_and_merge_requests] } - let(:labels) { [merge_requests] } - let(:add_labels) { [regression] } + it 'adds the label IDs to all issues passed' do + expect(issues.map(&:reload).map(&:label_ids)).to all(include(bug.id)) + end - it 'adds the label IDs to all issues passed' do - expect(issues.map(&:reload).map(&:label_ids)).to all(include(regression.id)) - end + it 'removes the label IDs from all issues passed' do + expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(merge_requests.id) + end - it 'ignores the label IDs parameter' do - expect(issues.map(&:reload).map(&:label_ids)).to all(include(bug.id)) + it 'does not update issues not passed in' do + expect(issue_bug_and_regression.label_ids).to contain_exactly(bug.id, regression.id) + end end - it 'does not update issues not passed in' do - expect(issue_no_labels.label_ids).to be_empty - end - end + context 'when add_label_ids and label_ids are passed' do + let(:issues) { [issue_all_labels, issue_bug_and_regression, issue_bug_and_merge_requests] } + let(:labels) { [merge_requests] } + let(:add_labels) { [regression] } - context 'when remove_label_ids and label_ids are passed' do - let(:issues) { [issue_no_labels, issue_bug_and_regression] } - let(:labels) { [merge_requests] } - let(:remove_labels) { [regression] } + it 'adds the label IDs to all issues passed' do + expect(issues.map(&:reload).map(&:label_ids)).to all(include(regression.id)) + end - it 'removes the label IDs from all issues passed' do - expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(regression.id) - end + it 'ignores the label IDs parameter' do + expect(issues.map(&:reload).map(&:label_ids)).to all(include(bug.id)) + end - it 'ignores the label IDs parameter' do - expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(merge_requests.id) + it 'does not update issues not passed in' do + expect(issue_no_labels.label_ids).to be_empty + end end - it 'does not update issues not passed in' do - expect(issue_all_labels.label_ids).to contain_exactly(bug.id, regression.id, merge_requests.id) - end - end + context 'when remove_label_ids and label_ids are passed' do + let(:issues) { [issue_no_labels, issue_bug_and_regression] } + let(:labels) { [merge_requests] } + let(:remove_labels) { [regression] } - context 'when add_label_ids, remove_label_ids, and label_ids are passed' do - let(:issues) { [issue_bug_and_merge_requests, issue_no_labels] } - let(:labels) { [regression] } - let(:add_labels) { [bug] } - let(:remove_labels) { [merge_requests] } + it 'removes the label IDs from all issues passed' do + expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(regression.id) + end - it 'adds the label IDs to all issues passed' do - expect(issues.map(&:reload).map(&:label_ids)).to all(include(bug.id)) - end + it 'ignores the label IDs parameter' do + expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(merge_requests.id) + end - it 'removes the label IDs from all issues passed' do - expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(merge_requests.id) + it 'does not update issues not passed in' do + expect(issue_all_labels.label_ids).to contain_exactly(bug.id, regression.id, merge_requests.id) + end end - it 'ignores the label IDs parameter' do - expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(regression.id) - end + context 'when add_label_ids, remove_label_ids, and label_ids are passed' do + let(:issues) { [issue_bug_and_merge_requests, issue_no_labels] } + let(:labels) { [regression] } + let(:add_labels) { [bug] } + let(:remove_labels) { [merge_requests] } - it 'does not update issues not passed in' do - expect(issue_bug_and_regression.label_ids).to contain_exactly(bug.id, regression.id) + it 'adds the label IDs to all issues passed' do + expect(issues.map(&:reload).map(&:label_ids)).to all(include(bug.id)) + end + + it 'removes the label IDs from all issues passed' do + expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(merge_requests.id) + end + + it 'ignores the label IDs parameter' do + expect(issues.map(&:reload).map(&:label_ids).flatten).not_to include(regression.id) + end + + it 'does not update issues not passed in' do + expect(issue_bug_and_regression.label_ids).to contain_exactly(bug.id, regression.id) + end end end - end - describe 'subscribe to issues' do - let(:issues) { create_list(:issue, 2, project: project) } + describe 'subscribe to issues' do + let(:issues) { create_list(:issue, 2, project: project) } - it 'subscribes the given user' do - bulk_update(issues, subscription_event: 'subscribe') + it 'subscribes the given user' do + bulk_update(issues, subscription_event: 'subscribe') - expect(issues).to all(be_subscribed(user, project)) + expect(issues).to all(be_subscribed(user, project)) + end end - end - describe 'unsubscribe from issues' do - let(:issues) do - create_list(:closed_issue, 2, project: project) do |issue| - issue.subscriptions.create(user: user, project: project, subscribed: true) + describe 'unsubscribe from issues' do + let(:issues) do + create_list(:closed_issue, 2, project: project) do |issue| + issue.subscriptions.create(user: user, project: project, subscribed: true) + end + end + + it 'unsubscribes the given user' do + bulk_update(issues, subscription_event: 'unsubscribe') + + issues.each do |issue| + expect(issue).not_to be_subscribed(user, project) + end end end + end - it 'unsubscribes the given user' do - bulk_update(issues, subscription_event: 'unsubscribe') + context 'with group issues' do + let(:group) { create(:group) } - issues.each do |issue| - expect(issue).not_to be_subscribed(user, project) + context 'updating milestone' do + let(:milestone) { create(:milestone, group: group) } + let(:project1) { create(:project, :repository, group: group) } + let(:project2) { create(:project, :repository, group: group) } + let(:issue1) { create(:issue, project: project1) } + let(:issue2) { create(:issue, project: project2) } + let(:issues) { [issue1, issue2] } + + before do + group.add_maintainer(user) end + + it_behaves_like 'updates milestones' end end end diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index 28fa5d12d9c..468e7c286d5 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -480,6 +480,22 @@ describe Issues::UpdateService, :mailer do update_issue(description: "- [x] Task 1\n- [X] Task 2") end + it 'does not check for spam on task status change' do + params = { + update_task: { + index: 1, + checked: false, + line_source: '- [x] Task 1', + line_number: 1 + } + } + service = described_class.new(project, user, params) + + expect(service).not_to receive(:spam_check) + + service.execute(issue) + end + it 'creates system note about task status change' do note1 = find_note('marked the task **Task 1** as completed') note2 = find_note('marked the task **Task 2** as completed') diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb index aa759ac9edc..22578436c18 100644 --- a/spec/services/merge_requests/merge_service_spec.rb +++ b/spec/services/merge_requests/merge_service_spec.rb @@ -214,6 +214,19 @@ describe MergeRequests::MergeService do allow(Rails.logger).to receive(:error) end + context 'when source is missing' do + it 'logs and saves error' do + allow(merge_request).to receive(:diff_head_sha) { nil } + + error_message = 'No source for merge' + + service.execute(merge_request) + + expect(merge_request.merge_error).to eq(error_message) + expect(Rails.logger).to have_received(:error).with(a_string_matching(error_message)) + end + end + it 'logs and saves error if there is an exception' do error_message = 'error message' diff --git a/spec/services/merge_requests/merge_to_ref_service_spec.rb b/spec/services/merge_requests/merge_to_ref_service_spec.rb index 14012b4ea2d..758679edc45 100644 --- a/spec/services/merge_requests/merge_to_ref_service_spec.rb +++ b/spec/services/merge_requests/merge_to_ref_service_spec.rb @@ -191,6 +191,19 @@ describe MergeRequests::MergeToRefService do it { expect(todo).not_to be_done } end + context 'when source is missing' do + it 'returns error' do + allow(merge_request).to receive(:diff_head_sha) { nil } + + error_message = 'No source for merge' + + result = service.execute(merge_request) + + expect(result[:status]).to eq(:error) + expect(result[:message]).to eq(error_message) + end + end + context 'when target ref is passed as a parameter' do let(:params) { { commit_message: 'merge train', target_ref: target_ref } } diff --git a/yarn.lock b/yarn.lock index 825a1785c3e..dc5e0662396 100644 --- a/yarn.lock +++ b/yarn.lock @@ -705,10 +705,10 @@ resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.67.0.tgz#c7b94eca13b99fd3aaa737fb6dcc0abc41d3c579" integrity sha512-hJOmWEs6RkjzyKkb1vc9wwKGZIBIP0coHkxu/KgOoxhBVudpGk4CH7xJ6UuB2TKpb0SEh5CC1CzRZfBYaFhsaA== -"@gitlab/ui@^5.3.2": - version "5.3.2" - resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-5.3.2.tgz#8ee906cf0586834de0077e165f25764c0bf8a9e9" - integrity sha512-VgxlDXqG2q+u72Km+/Ljdvjh0DzvljvsztiXTxnOO+Eb+/I26JBWfdboqFr3E02JzT8W4s4rRinhRttLWfcM/A== +"@gitlab/ui@^5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-5.5.0.tgz#2000b2ed0c3825dd8c4430191023f4d03c923ecb" + integrity sha512-6e/AFFLDk/gm4wKnHM9rcpTRqCsWkPKW/Vjsnd6h4wyfif2/TusHeIn/jedQxUaORbO/XZKzg4V5COhXXbCx4w== dependencies: "@babel/standalone" "^7.0.0" "@gitlab/vue-toasted" "^1.2.1" @@ -7476,9 +7476,9 @@ mississippi@^3.0.0: through2 "^2.0.0" mixin-deep@^1.2.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.1.tgz#a49e7268dce1a0d9698e45326c5626df3543d0fe" - integrity sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ== + version "1.3.2" + resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566" + integrity sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA== dependencies: for-in "^1.0.2" is-extendable "^1.0.1" |