diff options
78 files changed, 1130 insertions, 243 deletions
@@ -151,7 +151,7 @@ gem 'wikicloth', '0.8.1' gem 'asciidoctor', '~> 2.0.10' gem 'asciidoctor-include-ext', '~> 0.3.1', require: false gem 'asciidoctor-plantuml', '~> 0.0.12' -gem 'rouge', '~> 3.20.0' +gem 'rouge', '~> 3.21.0' gem 'truncato', '~> 0.7.11' gem 'bootstrap_form', '~> 4.2.0' gem 'nokogiri', '~> 1.10.9' diff --git a/Gemfile.lock b/Gemfile.lock index c1ea8ab4e1e..961e449889a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -907,7 +907,7 @@ GEM rexml (3.2.4) rinku (2.0.0) rotp (2.1.2) - rouge (3.20.0) + rouge (3.21.0) rqrcode (0.7.0) chunky_png rqrcode-rails3 (0.1.7) @@ -1370,7 +1370,7 @@ DEPENDENCIES request_store (~> 1.5) responders (~> 3.0) retriable (~> 3.1.2) - rouge (~> 3.20.0) + rouge (~> 3.21.0) rqrcode-rails3 (~> 0.1.7) rspec-parameterized rspec-rails (~> 4.0.0) diff --git a/app/assets/javascripts/issuables_list/components/issuable.vue b/app/assets/javascripts/issuables_list/components/issuable.vue index ed89cfea741..ab4f093d5da 100644 --- a/app/assets/javascripts/issuables_list/components/issuable.vue +++ b/app/assets/javascripts/issuables_list/components/issuable.vue @@ -97,6 +97,9 @@ export default { isJiraIssue() { return this.issuable.external_tracker === 'jira'; }, + linkTarget() { + return this.isJiraIssue ? '_blank' : null; + }, issueCreatedToday() { return getDayDifference(new Date(this.issuable.created_at), new Date()) < 1; }, @@ -239,11 +242,7 @@ export default { :title="$options.confidentialTooltipText" :aria-label="$options.confidentialTooltipText" /> - <gl-link - :href="issuable.web_url" - :target="isJiraIssue ? '_blank' : null" - data-testid="issuable-title" - > + <gl-link :href="issuable.web_url" :target="linkTarget" data-testid="issuable-title"> {{ issuable.title }} <gl-icon v-if="isJiraIssue" @@ -281,6 +280,7 @@ export default { ref="openedAgoByContainer" v-bind="popoverDataAttrs" :href="issuableAuthor.web_url" + :target="linkTarget" > {{ issuableAuthor.name }} </gl-link> @@ -340,8 +340,8 @@ export default { <!-- Issuable meta --> <div class="flex-shrink-0 d-flex flex-column align-items-end justify-content-center"> <div class="controls d-flex"> - <span v-if="isJiraIssue"> </span> - <span v-if="isClosed" class="issuable-status">{{ __('CLOSED') }}</span> + <span v-if="isJiraIssue" data-testid="issuable-status">{{ issuable.status }}</span> + <span v-else-if="isClosed" class="issuable-status">{{ __('CLOSED') }}</span> <issue-assignees :assignees="issuable.assignees" diff --git a/app/assets/javascripts/repository/queries/getFiles.query.graphql b/app/assets/javascripts/repository/queries/getFiles.query.graphql index 2aaf5066b4a..feb89df0492 100644 --- a/app/assets/javascripts/repository/queries/getFiles.query.graphql +++ b/app/assets/javascripts/repository/queries/getFiles.query.graphql @@ -45,6 +45,7 @@ query getFiles( edges { node { ...TreeEntry + mode webUrl lfsOid } diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue index 34efc6afc6f..04090213218 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue @@ -83,6 +83,7 @@ export default { return { initialRender: true, recentSearchesPromise: null, + recentSearches: [], filterValue: this.initialFilterValue, selectedSortOption, selectedSortDirection, @@ -180,11 +181,9 @@ export default { this.recentSearchesStore.state.recentSearches.concat(searches), ); this.recentSearchesService.save(resultantSearches); + this.recentSearches = resultantSearches; }); }, - getRecentSearches() { - return this.recentSearchesStore?.state.recentSearches; - }, handleSortOptionClick(sortBy) { this.selectedSortOption = sortBy; this.$emit('onSort', sortBy.sortDirection[this.selectedSortDirection]); @@ -196,9 +195,13 @@ export default { : SortDirection.ascending; this.$emit('onSort', this.selectedSortOption.sortDirection[this.selectedSortDirection]); }, + handleHistoryItemSelected(filters) { + this.$emit('onFilter', filters); + }, handleClearHistory() { const resultantSearches = this.recentSearchesStore.setRecentSearches([]); this.recentSearchesService.save(resultantSearches); + this.recentSearches = []; }, handleFilterSubmit(filters) { if (this.recentSearchesStorageKey) { @@ -207,6 +210,7 @@ export default { if (filters.length) { const resultantSearches = this.recentSearchesStore.addRecentSearch(filters); this.recentSearchesService.save(resultantSearches); + this.recentSearches = resultantSearches; } }) .catch(() => { @@ -225,16 +229,15 @@ export default { v-model="filterValue" :placeholder="searchInputPlaceholder" :available-tokens="tokens" - :history-items="getRecentSearches()" + :history-items="recentSearches" class="flex-grow-1" - @history-item-selected="$emit('onFilter', filters)" + @history-item-selected="handleHistoryItemSelected" @clear-history="handleClearHistory" @submit="handleFilterSubmit" - @clear="$emit('onFilter', [])" > <template #history-item="{ historyItem }"> - <template v-for="token in historyItem"> - <span v-if="typeof token === 'string'" :key="token" class="gl-px-1">"{{ token }}"</span> + <template v-for="(token, index) in historyItem"> + <span v-if="typeof token === 'string'" :key="index" class="gl-px-1">"{{ token }}"</span> <span v-else :key="`${token.type}-${token.value.data}`" class="gl-px-1"> <span v-if="tokenTitles[token.type]" >{{ tokenTitles[token.type] }} :{{ token.value.operator }}</span diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index d76cddfb46e..ef50bcfc2f9 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -253,13 +253,6 @@ } .stage-cell { - &.table-section { - @include media-breakpoint-up(md) { - min-width: 160px; /* Hack alert: Without this the mini graph pipeline won't work properly*/ - margin-right: -4px; - } - } - .mini-pipeline-graph-dropdown-toggle { svg { height: $ci-action-icon-size; diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb index 0df201ab506..99fa17e202a 100644 --- a/app/controllers/autocomplete_controller.rb +++ b/app/controllers/autocomplete_controller.rb @@ -4,10 +4,6 @@ class AutocompleteController < ApplicationController skip_before_action :authenticate_user!, only: [:users, :award_emojis, :merge_request_target_branches] def users - project = Autocomplete::ProjectFinder - .new(current_user, params) - .execute - group = Autocomplete::GroupFinder .new(current_user, project, params) .execute @@ -50,8 +46,20 @@ class AutocompleteController < ApplicationController end end + def deploy_keys_with_owners + deploy_keys = DeployKeys::CollectKeysService.new(project, current_user).execute + + render json: DeployKeySerializer.new.represent(deploy_keys, { with_owner: true, user: current_user }) + end + private + def project + @project ||= Autocomplete::ProjectFinder + .new(current_user, params) + .execute + end + def target_branch_params params.permit(:group_id, :project_id).select { |_, v| v.present? } end diff --git a/app/graphql/types/tree/blob_type.rb b/app/graphql/types/tree/blob_type.rb index 22349203519..36cae756a0d 100644 --- a/app/graphql/types/tree/blob_type.rb +++ b/app/graphql/types/tree/blob_type.rb @@ -17,6 +17,8 @@ module Types resolve: -> (blob, args, ctx) do Gitlab::Graphql::Loaders::BatchLfsOidLoader.new(blob.repository, blob.id).find end + field :mode, GraphQL::STRING_TYPE, null: true, + description: 'Blob mode in numeric format' # rubocop: enable Graphql/AuthorizeTypes end end diff --git a/app/models/audit_event.rb b/app/models/audit_event.rb index 392f8d647a1..3d36214a178 100644 --- a/app/models/audit_event.rb +++ b/app/models/audit_event.rb @@ -19,7 +19,15 @@ class AuditEvent < ApplicationRecord scope :by_entity_id, -> (entity_id) { where(entity_id: entity_id) } scope :by_author_id, -> (author_id) { where(author_id: author_id) } + PARALLEL_PERSISTENCE_COLUMNS = [:author_name].freeze + after_initialize :initialize_details + # Note: The intention is to remove this once refactoring of AuditEvent + # has proceeded further. + # + # See further details in the epic: + # https://gitlab.com/groups/gitlab-org/-/epics/2765 + after_validation :parallel_persist def self.order_by(method) case method.to_s @@ -55,6 +63,10 @@ class AuditEvent < ApplicationRecord def default_author_value ::Gitlab::Audit::NullAuthor.for(author_id, (self[:author_name] || details[:author_name])) end + + def parallel_persist + PARALLEL_PERSISTENCE_COLUMNS.each { |col| self[col] = details[col] } + end end AuditEvent.prepend_if_ee('EE::AuditEvent') diff --git a/app/models/ci/build_need.rb b/app/models/ci/build_need.rb index 0b243c20e67..b977a5f4419 100644 --- a/app/models/ci/build_need.rb +++ b/app/models/ci/build_need.rb @@ -4,6 +4,8 @@ module Ci class BuildNeed < ApplicationRecord extend Gitlab::Ci::Model + include BulkInsertSafe + belongs_to :build, class_name: "Ci::Build", foreign_key: :build_id, inverse_of: :needs validates :build, presence: true diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index cb22a9268fb..c85292feb25 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -6,6 +6,7 @@ class CommitStatus < ApplicationRecord include AfterCommitQueue include Presentable include EnumWithNil + include BulkInsertableAssociations self.table_name = 'ci_builds' diff --git a/app/models/deploy_keys_project.rb b/app/models/deploy_keys_project.rb index 40c66d5bc4c..a9cc56a7246 100644 --- a/app/models/deploy_keys_project.rb +++ b/app/models/deploy_keys_project.rb @@ -6,6 +6,7 @@ class DeployKeysProject < ApplicationRecord scope :without_project_deleted, -> { joins(:project).where(projects: { pending_delete: false }) } scope :in_project, ->(project) { where(project: project) } scope :with_write_access, -> { where(can_push: true) } + scope :with_deploy_keys, -> { includes(:deploy_key) } accepts_nested_attributes_for :deploy_key diff --git a/app/models/resource_state_event.rb b/app/models/resource_state_event.rb index 1d6573b180f..f8f7965a921 100644 --- a/app/models/resource_state_event.rb +++ b/app/models/resource_state_event.rb @@ -6,6 +6,8 @@ class ResourceStateEvent < ResourceEvent validate :exactly_one_issuable + belongs_to :source_merge_request, class_name: 'MergeRequest', foreign_key: :source_merge_request_id + # state is used for issue and merge request states. enum state: Issue.available_states.merge(MergeRequest.available_states).merge(reopened: 5) diff --git a/app/models/state_note.rb b/app/models/state_note.rb index cbcb1c2b49d..5e35f15aac4 100644 --- a/app/models/state_note.rb +++ b/app/models/state_note.rb @@ -1,19 +1,47 @@ # frozen_string_literal: true class StateNote < SyntheticNote + include Gitlab::Utils::StrongMemoize + def self.from_event(event, resource: nil, resource_parent: nil) - attrs = note_attributes(event.state, event, resource, resource_parent) + attrs = note_attributes(action_by(event), event, resource, resource_parent) StateNote.new(attrs) end def note_html - @note_html ||= "<p dir=\"auto\">#{note_text(html: true)}</p>" + @note_html ||= Banzai::Renderer.cacheless_render_field(self, :note, { group: group, project: project }) end private def note_text(html: false) - event.state + if event.state == 'closed' + if event.close_after_error_tracking_resolve + return 'resolved the corresponding error and closed the issue.' + end + + if event.close_auto_resolve_prometheus_alert + return 'automatically closed this issue because the alert resolved.' + end + end + + body = event.state.dup + body << " via #{event_source.gfm_reference(project)}" if event_source + body + end + + def event_source + strong_memoize(:event_source) do + if event.source_commit + project&.commit(event.source_commit) + else + event.source_merge_request + end + end + end + + def self.action_by(event) + event.state == 'reopened' ? 'opened' : event.state end end diff --git a/app/models/synthetic_note.rb b/app/models/synthetic_note.rb index 3017140f871..dea7165af9f 100644 --- a/app/models/synthetic_note.rb +++ b/app/models/synthetic_note.rb @@ -3,20 +3,18 @@ class SyntheticNote < Note attr_accessor :resource_parent, :event - self.abstract_class = true - def self.note_attributes(action, event, resource, resource_parent) resource ||= event.resource attrs = { - system: true, - author: event.user, - created_at: event.created_at, - discussion_id: event.discussion_id, - noteable: resource, - event: event, - system_note_metadata: ::SystemNoteMetadata.new(action: action), - resource_parent: resource_parent + system: true, + author: event.user, + created_at: event.created_at, + discussion_id: event.discussion_id, + noteable: resource, + event: event, + system_note_metadata: ::SystemNoteMetadata.new(action: action), + resource_parent: resource_parent } if resource_parent.is_a?(Project) diff --git a/app/serializers/deploy_key_entity.rb b/app/serializers/deploy_key_entity.rb index 653316ce4d2..486189b84ca 100644 --- a/app/serializers/deploy_key_entity.rb +++ b/app/serializers/deploy_key_entity.rb @@ -16,6 +16,7 @@ class DeployKeyEntity < Grape::Entity end end expose :can_edit + expose :user, as: :owner, using: ::API::Entities::UserBasic, if: -> (_, opts) { can_read_owner?(opts) } private @@ -24,6 +25,10 @@ class DeployKeyEntity < Grape::Entity Ability.allowed?(options[:user], :update_deploy_keys_project, object.deploy_keys_project_for(options[:project])) end + def can_read_owner?(opts) + opts[:with_owner] && Ability.allowed?(options[:user], :read_user, object.user) + end + def allowed_to_read_project?(project) if options[:readable_project_ids] options[:readable_project_ids].include?(project.id) diff --git a/app/services/ci/process_pipeline_service.rb b/app/services/ci/process_pipeline_service.rb index 80ebe5f5eb6..1f24dce0458 100644 --- a/app/services/ci/process_pipeline_service.rb +++ b/app/services/ci/process_pipeline_service.rb @@ -9,6 +9,8 @@ module Ci end def execute(trigger_build_ids = nil, initial_process: false) + increment_processing_counter + update_retried if ::Gitlab::Ci::Features.atomic_processing?(pipeline.project) @@ -22,6 +24,10 @@ module Ci end end + def metrics + @metrics ||= ::Gitlab::Ci::Pipeline::Metrics.new + end + private # This method is for compatibility and data consistency and should be removed with 9.3 version of GitLab @@ -43,5 +49,9 @@ module Ci .update_all(retried: true) if latest_statuses.any? end # rubocop: enable CodeReuse/ActiveRecord + + def increment_processing_counter + metrics.pipeline_processing_events_counter.increment + end end end diff --git a/app/services/ci/retry_build_service.rb b/app/services/ci/retry_build_service.rb index e08a37792a9..60b3d28b0c5 100644 --- a/app/services/ci/retry_build_service.rb +++ b/app/services/ci/retry_build_service.rb @@ -55,7 +55,9 @@ module Ci build = project.builds.new(attributes) build.assign_attributes(::Gitlab::Ci::Pipeline::Seed::Build.environment_attributes_for(build)) build.retried = false - build.save! + BulkInsertableAssociations.with_bulk_insert(enabled: ::Gitlab::Ci::Features.bulk_insert_on_create?(project)) do + build.save! + end build end end diff --git a/app/services/deploy_keys/collect_keys_service.rb b/app/services/deploy_keys/collect_keys_service.rb new file mode 100644 index 00000000000..2ef49bf0f30 --- /dev/null +++ b/app/services/deploy_keys/collect_keys_service.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module DeployKeys + class CollectKeysService + def initialize(project, current_user) + @project = project + @current_user = current_user + end + + def execute + return [] unless current_user && project && user_can_read_project + + project.deploy_keys_projects + .with_deploy_keys + .with_write_access + .map(&:deploy_key) + end + + private + + def user_can_read_project + Ability.allowed?(current_user, :read_project, project) + end + + attr_reader :project, :current_user + end +end diff --git a/app/services/event_create_service.rb b/app/services/event_create_service.rb index 5e184e41885..faceefd8114 100644 --- a/app/services/event_create_service.rb +++ b/app/services/event_create_service.rb @@ -11,44 +11,30 @@ class EventCreateService IllegalActionError = Class.new(StandardError) def open_issue(issue, current_user) - create_resource_event(issue, current_user, :opened) - create_record_event(issue, current_user, :created) end def close_issue(issue, current_user) - create_resource_event(issue, current_user, :closed) - create_record_event(issue, current_user, :closed) end def reopen_issue(issue, current_user) - create_resource_event(issue, current_user, :reopened) - create_record_event(issue, current_user, :reopened) end def open_mr(merge_request, current_user) - create_resource_event(merge_request, current_user, :opened) - create_record_event(merge_request, current_user, :created) end def close_mr(merge_request, current_user) - create_resource_event(merge_request, current_user, :closed) - create_record_event(merge_request, current_user, :closed) end def reopen_mr(merge_request, current_user) - create_resource_event(merge_request, current_user, :reopened) - create_record_event(merge_request, current_user, :reopened) end def merge_mr(merge_request, current_user) - create_resource_event(merge_request, current_user, :merged) - create_record_event(merge_request, current_user, :merged) end @@ -220,18 +206,6 @@ class EventCreateService { resource_parent_attr => resource_parent.id } end - - def create_resource_event(issuable, current_user, status) - return unless state_change_tracking_enabled?(issuable) - - ResourceEvents::ChangeStateService.new(resource: issuable, user: current_user) - .execute(status) - end - - def state_change_tracking_enabled?(issuable) - issuable&.respond_to?(:resource_state_events) && - ::Feature.enabled?(:track_resource_state_change_events, issuable&.project) - end end EventCreateService.prepend_if_ee('EE::EventCreateService') diff --git a/app/services/resource_events/change_state_service.rb b/app/services/resource_events/change_state_service.rb index 8beb76d8aee..202972c1efd 100644 --- a/app/services/resource_events/change_state_service.rb +++ b/app/services/resource_events/change_state_service.rb @@ -8,12 +8,18 @@ module ResourceEvents @user, @resource = user, resource end - def execute(state) + def execute(params) + @params = params + ResourceStateEvent.create( user: user, issue: issue, merge_request: merge_request, + source_commit: commit_id_of(mentionable_source), + source_merge_request_id: merge_request_id_of(mentionable_source), state: ResourceStateEvent.states[state], + close_after_error_tracking_resolve: close_after_error_tracking_resolve, + close_auto_resolve_prometheus_alert: close_auto_resolve_prometheus_alert, created_at: Time.zone.now) resource.expire_note_etag_cache @@ -21,6 +27,36 @@ module ResourceEvents private + attr_reader :params + + def close_auto_resolve_prometheus_alert + params[:close_auto_resolve_prometheus_alert] || false + end + + def close_after_error_tracking_resolve + params[:close_after_error_tracking_resolve] || false + end + + def state + params[:status] + end + + def mentionable_source + params[:mentionable_source] + end + + def commit_id_of(mentionable_source) + return unless mentionable_source.is_a?(Commit) + + mentionable_source.id[0...40] + end + + def merge_request_id_of(mentionable_source) + return unless mentionable_source.is_a?(MergeRequest) + + mentionable_source.id + end + def issue return unless resource.is_a?(Issue) diff --git a/app/services/system_notes/issuables_service.rb b/app/services/system_notes/issuables_service.rb index 7d7ee8d829e..76261aa716e 100644 --- a/app/services/system_notes/issuables_service.rb +++ b/app/services/system_notes/issuables_service.rb @@ -228,7 +228,9 @@ module SystemNotes # A state event which results in a synthetic note will be # created by EventCreateService if change event tracking # is enabled. - unless state_change_tracking_enabled? + if state_change_tracking_enabled? + create_resource_state_event(status: status, mentionable_source: source) + else create_note(NoteSummary.new(noteable, project, author, body, action: action)) end end @@ -288,15 +290,23 @@ module SystemNotes end def close_after_error_tracking_resolve - body = _('resolved the corresponding error and closed the issue.') + if state_change_tracking_enabled? + create_resource_state_event(status: 'closed', close_after_error_tracking_resolve: true) + else + body = 'resolved the corresponding error and closed the issue.' - create_note(NoteSummary.new(noteable, project, author, body, action: 'closed')) + create_note(NoteSummary.new(noteable, project, author, body, action: 'closed')) + end end def auto_resolve_prometheus_alert - body = 'automatically closed this issue because the alert resolved.' + if state_change_tracking_enabled? + create_resource_state_event(status: 'closed', close_auto_resolve_prometheus_alert: true) + else + body = 'automatically closed this issue because the alert resolved.' - create_note(NoteSummary.new(noteable, project, author, body, action: 'closed')) + create_note(NoteSummary.new(noteable, project, author, body, action: 'closed')) + end end private @@ -324,6 +334,11 @@ module SystemNotes note_text =~ /\A#{cross_reference_note_prefix}/i end + def create_resource_state_event(params) + ResourceEvents::ChangeStateService.new(resource: noteable, user: author) + .execute(params) + end + def state_change_tracking_enabled? noteable.respond_to?(:resource_state_events) && ::Feature.enabled?(:track_resource_state_change_events, noteable.project) diff --git a/app/views/admin/application_settings/repository.html.haml b/app/views/admin/application_settings/repository.html.haml index 11de79cf4a2..33a6715d424 100644 --- a/app/views/admin/application_settings/repository.html.haml +++ b/app/views/admin/application_settings/repository.html.haml @@ -2,7 +2,7 @@ - page_title _("Repository") - @content_class = "limit-container-width" unless fluid_layout -- if Feature.enabled?(:global_default_branch_name) +- if Feature.enabled?(:global_default_branch_name, default_enabled: true) %section.settings.as-default-branch-name.no-animate#js-default-branch-name{ class: ('expanded' if expanded_by_default?) } .settings-header %h4 diff --git a/bin/rspec-stackprof b/bin/rspec-stackprof index 8058d165196..3bef45c607c 100755 --- a/bin/rspec-stackprof +++ b/bin/rspec-stackprof @@ -8,9 +8,10 @@ require 'spec_helper' filename = ARGV[0].split('/').last interval = ENV.fetch('INTERVAL', 1000).to_i limit = ENV.fetch('LIMIT', 20) +raw = ENV.fetch('RAW', false) == 'true' output_file = "tmp/#{filename}.dump" -StackProf.run(mode: :wall, out: output_file, interval: interval) do +StackProf.run(mode: :wall, out: output_file, interval: interval, raw: raw) do RSpec::Core::Runner.run(ARGV, $stderr, $stdout) end diff --git a/changelogs/unreleased/30769-deploy-keys-push-protected-branches.yml b/changelogs/unreleased/30769-deploy-keys-push-protected-branches.yml new file mode 100644 index 00000000000..5c21a1d6496 --- /dev/null +++ b/changelogs/unreleased/30769-deploy-keys-push-protected-branches.yml @@ -0,0 +1,5 @@ +--- +title: Expose project deploy keys for autocompletion +merge_request: 34875 +author: +type: added diff --git a/changelogs/unreleased/branch-name-default-to-true.yml b/changelogs/unreleased/branch-name-default-to-true.yml new file mode 100644 index 00000000000..58a2d015a02 --- /dev/null +++ b/changelogs/unreleased/branch-name-default-to-true.yml @@ -0,0 +1,6 @@ +--- +title: Default the feature flag to true to always show the default initial branch + name setting +merge_request: 36889 +author: +type: added diff --git a/changelogs/unreleased/create-state-events-pd.yml b/changelogs/unreleased/create-state-events-pd.yml new file mode 100644 index 00000000000..9c20e3f73ce --- /dev/null +++ b/changelogs/unreleased/create-state-events-pd.yml @@ -0,0 +1,5 @@ +--- +title: Add source to resource state events +merge_request: 32924 +author: +type: other diff --git a/changelogs/unreleased/enable-bulk-insert-for-needs.yml b/changelogs/unreleased/enable-bulk-insert-for-needs.yml new file mode 100644 index 00000000000..d4b8f8b958e --- /dev/null +++ b/changelogs/unreleased/enable-bulk-insert-for-needs.yml @@ -0,0 +1,5 @@ +--- +title: Enable BulkInsertSafe on Ci::BuildNeed +merge_request: 36815 +author: +type: performance diff --git a/changelogs/unreleased/reduce_pipeline_status_gitaly_call.yml b/changelogs/unreleased/reduce_pipeline_status_gitaly_call.yml new file mode 100644 index 00000000000..6534546dee6 --- /dev/null +++ b/changelogs/unreleased/reduce_pipeline_status_gitaly_call.yml @@ -0,0 +1,5 @@ +--- +title: Remove need to call commit (gitaly call) in ProjectPipelineStatus +merge_request: 33712 +author: +type: performance diff --git a/changelogs/unreleased/say-no-to-hacks.yml b/changelogs/unreleased/say-no-to-hacks.yml new file mode 100644 index 00000000000..7d9c342121f --- /dev/null +++ b/changelogs/unreleased/say-no-to-hacks.yml @@ -0,0 +1,5 @@ +--- +title: Removes fixes that broke the pipeline table +merge_request: 36803 +author: +type: fixed diff --git a/changelogs/unreleased/suppress-progress-on-pulling-image-in-builtin-templates.yml b/changelogs/unreleased/suppress-progress-on-pulling-image-in-builtin-templates.yml new file mode 100644 index 00000000000..c9cbc4181a0 --- /dev/null +++ b/changelogs/unreleased/suppress-progress-on-pulling-image-in-builtin-templates.yml @@ -0,0 +1,5 @@ +--- +title: Suppress progress on docker pulling in builtin templates +merge_request: 35253 +author: Takuya Noguchi +type: other diff --git a/changelogs/unreleased/symlink-icon-graphql-file-mode.yml b/changelogs/unreleased/symlink-icon-graphql-file-mode.yml new file mode 100644 index 00000000000..b84fb22c24b --- /dev/null +++ b/changelogs/unreleased/symlink-icon-graphql-file-mode.yml @@ -0,0 +1,5 @@ +--- +title: Expose blob mode in GraphQL for repository files +merge_request: 36488 +author: +type: other diff --git a/changelogs/unreleased/update-rouge-3-21.yml b/changelogs/unreleased/update-rouge-3-21.yml new file mode 100644 index 00000000000..69a204b87c1 --- /dev/null +++ b/changelogs/unreleased/update-rouge-3-21.yml @@ -0,0 +1,5 @@ +--- +title: Update Rouge to v3.21.0 +merge_request: 36942 +author: +type: other diff --git a/config/routes.rb b/config/routes.rb index 36e995bc0af..03a86d47646 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -76,6 +76,7 @@ Rails.application.routes.draw do get '/autocomplete/projects' => 'autocomplete#projects' get '/autocomplete/award_emojis' => 'autocomplete#award_emojis' get '/autocomplete/merge_request_target_branches' => 'autocomplete#merge_request_target_branches' + get '/autocomplete/deploy_keys_with_owners' => 'autocomplete#deploy_keys_with_owners' Gitlab.ee do get '/autocomplete/project_groups' => 'autocomplete#project_groups' diff --git a/db/migrate/20200524104346_add_source_to_resource_state_event.rb b/db/migrate/20200524104346_add_source_to_resource_state_event.rb new file mode 100644 index 00000000000..a1d1575bb02 --- /dev/null +++ b/db/migrate/20200524104346_add_source_to_resource_state_event.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +class AddSourceToResourceStateEvent < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + unless column_exists?(:resource_state_events, :source_commit) + add_column :resource_state_events, :source_commit, :text + end + + add_text_limit :resource_state_events, :source_commit, 40 + end + + def down + remove_column :resource_state_events, :source_commit + end +end diff --git a/db/migrate/20200615141554_add_closed_by_fields_to_resource_state_events.rb b/db/migrate/20200615141554_add_closed_by_fields_to_resource_state_events.rb new file mode 100644 index 00000000000..ba11e64e667 --- /dev/null +++ b/db/migrate/20200615141554_add_closed_by_fields_to_resource_state_events.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class AddClosedByFieldsToResourceStateEvents < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + add_column :resource_state_events, :close_after_error_tracking_resolve, :boolean, default: false, null: false + add_column :resource_state_events, :close_auto_resolve_prometheus_alert, :boolean, default: false, null: false + end + + def down + remove_column :resource_state_events, :close_auto_resolve_prometheus_alert, :boolean + remove_column :resource_state_events, :close_after_error_tracking_resolve, :boolean + end +end diff --git a/db/migrate/20200617205000_add_deploy_key_id_to_push_access_levels.rb b/db/migrate/20200617205000_add_deploy_key_id_to_push_access_levels.rb new file mode 100644 index 00000000000..11b92c2a321 --- /dev/null +++ b/db/migrate/20200617205000_add_deploy_key_id_to_push_access_levels.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +class AddDeployKeyIdToPushAccessLevels < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + unless column_exists?(:protected_branch_push_access_levels, :deploy_key_id) + add_column :protected_branch_push_access_levels, :deploy_key_id, :integer + end + + add_concurrent_foreign_key :protected_branch_push_access_levels, :keys, column: :deploy_key_id, on_delete: :cascade + add_concurrent_index :protected_branch_push_access_levels, :deploy_key_id, name: 'index_deploy_key_id_on_protected_branch_push_access_levels' + end + + def down + remove_column :protected_branch_push_access_levels, :deploy_key_id + end +end diff --git a/db/migrate/20200623073431_add_source_merge_request_id_to_resource_state_events.rb b/db/migrate/20200623073431_add_source_merge_request_id_to_resource_state_events.rb new file mode 100644 index 00000000000..8970797d3c0 --- /dev/null +++ b/db/migrate/20200623073431_add_source_merge_request_id_to_resource_state_events.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +class AddSourceMergeRequestIdToResourceStateEvents < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + INDEX_NAME = 'index_resource_state_events_on_source_merge_request_id' + + DOWNTIME = false + + disable_ddl_transaction! + + def up + unless column_exists?(:resource_state_events, :source_merge_request_id) + add_column :resource_state_events, :source_merge_request_id, :bigint + end + + unless index_exists?(:resource_state_events, :source_merge_request_id, name: INDEX_NAME) + add_index :resource_state_events, :source_merge_request_id, name: INDEX_NAME # rubocop: disable Migration/AddIndex + end + + unless foreign_key_exists?(:resource_state_events, :merge_requests, column: :source_merge_request_id) + with_lock_retries do + add_foreign_key :resource_state_events, :merge_requests, column: :source_merge_request_id, on_delete: :nullify # rubocop:disable Migration/AddConcurrentForeignKey + end + end + end + + def down + with_lock_retries do + remove_column :resource_state_events, :source_merge_request_id + end + end +end diff --git a/db/structure.sql b/db/structure.sql index 3afb7fc6ccb..0c6d71b5021 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -14502,7 +14502,8 @@ CREATE TABLE public.protected_branch_push_access_levels ( created_at timestamp without time zone NOT NULL, updated_at timestamp without time zone NOT NULL, user_id integer, - group_id integer + group_id integer, + deploy_key_id integer ); CREATE SEQUENCE public.protected_branch_push_access_levels_id_seq @@ -14854,6 +14855,11 @@ CREATE TABLE public.resource_state_events ( created_at timestamp with time zone NOT NULL, state smallint NOT NULL, epic_id integer, + source_commit text, + close_after_error_tracking_resolve boolean DEFAULT false NOT NULL, + close_auto_resolve_prometheus_alert boolean DEFAULT false NOT NULL, + source_merge_request_id bigint, + CONSTRAINT check_f0bcfaa3a2 CHECK ((char_length(source_commit) <= 40)), CONSTRAINT state_events_must_belong_to_issue_or_merge_request_or_epic CHECK ((((issue_id <> NULL::bigint) AND (merge_request_id IS NULL) AND (epic_id IS NULL)) OR ((issue_id IS NULL) AND (merge_request_id <> NULL::bigint) AND (epic_id IS NULL)) OR ((issue_id IS NULL) AND (merge_request_id IS NULL) AND (epic_id <> NULL::integer)))) ); @@ -19027,6 +19033,8 @@ CREATE INDEX index_dependency_proxy_blobs_on_group_id_and_file_name ON public.de CREATE INDEX index_dependency_proxy_group_settings_on_group_id ON public.dependency_proxy_group_settings USING btree (group_id); +CREATE INDEX index_deploy_key_id_on_protected_branch_push_access_levels ON public.protected_branch_push_access_levels USING btree (deploy_key_id); + CREATE INDEX index_deploy_keys_projects_on_deploy_key_id ON public.deploy_keys_projects USING btree (deploy_key_id); CREATE INDEX index_deploy_keys_projects_on_project_id ON public.deploy_keys_projects USING btree (project_id); @@ -20151,6 +20159,8 @@ CREATE INDEX index_resource_state_events_on_issue_id_and_created_at ON public.re CREATE INDEX index_resource_state_events_on_merge_request_id ON public.resource_state_events USING btree (merge_request_id); +CREATE INDEX index_resource_state_events_on_source_merge_request_id ON public.resource_state_events USING btree (source_merge_request_id); + CREATE INDEX index_resource_state_events_on_user_id ON public.resource_state_events USING btree (user_id); CREATE INDEX index_resource_weight_events_on_issue_id_and_created_at ON public.resource_weight_events USING btree (issue_id, created_at); @@ -20910,6 +20920,9 @@ ALTER TABLE ONLY public.vulnerabilities ALTER TABLE ONLY public.vulnerabilities ADD CONSTRAINT fk_131d289c65 FOREIGN KEY (milestone_id) REFERENCES public.milestones(id) ON DELETE SET NULL; +ALTER TABLE ONLY public.protected_branch_push_access_levels + ADD CONSTRAINT fk_15d2a7a4ae FOREIGN KEY (deploy_key_id) REFERENCES public.keys(id) ON DELETE CASCADE; + ALTER TABLE ONLY public.internal_ids ADD CONSTRAINT fk_162941d509 FOREIGN KEY (namespace_id) REFERENCES public.namespaces(id) ON DELETE CASCADE; @@ -22053,6 +22066,9 @@ ALTER TABLE ONLY public.operations_scopes ALTER TABLE ONLY public.milestone_releases ADD CONSTRAINT fk_rails_7ae0756a2d FOREIGN KEY (milestone_id) REFERENCES public.milestones(id) ON DELETE CASCADE; +ALTER TABLE ONLY public.resource_state_events + ADD CONSTRAINT fk_rails_7ddc5f7457 FOREIGN KEY (source_merge_request_id) REFERENCES public.merge_requests(id) ON DELETE SET NULL; + ALTER TABLE ONLY public.application_settings ADD CONSTRAINT fk_rails_7e112a9599 FOREIGN KEY (instance_administration_project_id) REFERENCES public.projects(id) ON DELETE SET NULL; @@ -23618,6 +23634,7 @@ COPY "schema_migrations" (version) FROM STDIN; 20200521225346 20200522205606 20200522235146 +20200524104346 20200525114553 20200525121014 20200525144525 @@ -23676,6 +23693,7 @@ COPY "schema_migrations" (version) FROM STDIN; 20200615111857 20200615121217 20200615123055 +20200615141554 20200615193524 20200615232735 20200615234047 @@ -23688,6 +23706,7 @@ COPY "schema_migrations" (version) FROM STDIN; 20200617001848 20200617002030 20200617150041 +20200617205000 20200618105638 20200618134223 20200618134723 @@ -23704,6 +23723,7 @@ COPY "schema_migrations" (version) FROM STDIN; 20200622235737 20200623000148 20200623000320 +20200623073431 20200623090030 20200623121135 20200623141217 diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql index 2d5c7f0691a..200cc24c918 100644 --- a/doc/api/graphql/reference/gitlab_schema.graphql +++ b/doc/api/graphql/reference/gitlab_schema.graphql @@ -794,6 +794,11 @@ type Blob implements Entry { lfsOid: String """ + Blob mode in numeric format + """ + mode: String + + """ Name of the entry """ name: String! @@ -5115,7 +5120,7 @@ type Group { iid: ID """ - Whether to include ancestor Iterations. Defaults to true + Whether to include ancestor iterations. Defaults to true """ includeAncestors: Boolean diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json index f240703ba18..93fbf61895b 100644 --- a/doc/api/graphql/reference/gitlab_schema.json +++ b/doc/api/graphql/reference/gitlab_schema.json @@ -2057,6 +2057,20 @@ "deprecationReason": null }, { + "name": "mode", + "description": "Blob mode in numeric format", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { "name": "name", "description": "Name of the entry", "args": [ @@ -14142,7 +14156,7 @@ }, { "name": "includeAncestors", - "description": "Whether to include ancestor Iterations. Defaults to true", + "description": "Whether to include ancestor iterations. Defaults to true", "type": { "kind": "SCALAR", "name": "Boolean", diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index c80337175c9..f33ef36504f 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -160,6 +160,7 @@ Autogenerated return type of AwardEmojiToggle | `flatPath` | String! | Flat path of the entry | | `id` | ID! | ID of the entry | | `lfsOid` | String | LFS ID of the blob | +| `mode` | String | Blob mode in numeric format | | `name` | String! | Name of the entry | | `path` | String! | Path of the entry | | `sha` | String! | Last commit sha for the entry | diff --git a/doc/api/services.md b/doc/api/services.md index c4bd5f86e43..4052fd22641 100644 --- a/doc/api/services.md +++ b/doc/api/services.md @@ -496,12 +496,6 @@ GET /projects/:id/services/emails-on-push ## Confluence service > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/220934) in GitLab 13.2. -> - It's deployed behind a feature flag, disabled by default. -> - It's disabled on GitLab.com. -> - It's able to be enabled or disabled per-project -> - It's not recommended for production use. -> - To use it in GitLab self-managed instances, ask a GitLab administrator to - [enable it](#enable-or-disable-the-confluence-service-core-only). **(CORE ONLY)** Replaces the link to the internal wiki with a link to a Confluence Cloud Workspace. @@ -535,31 +529,6 @@ Get Confluence service settings for a project. GET /projects/:id/services/confluence ``` -### Enable or disable the Confluence service **(CORE ONLY)** - -The Confluence service is under development and not ready for production use. It is -deployed behind a feature flag that is **disabled by default**. -[GitLab administrators with access to the GitLab Rails console](../administration/feature_flags.md) -can enable it for your instance. The Confluence service can be enabled or disabled per-project - -To enable it: - -```ruby -# Instance-wide -Feature.enable(:confluence_integration) -# or by project -Feature.enable(:confluence_integration, Project.find(<project id>)) -``` - -To disable it: - -```ruby -# Instance-wide -Feature.disable(:confluence_integration) -# or by project -Feature.disable(:confluence_integration, Project.find(<project id>)) -``` - ## External Wiki Replaces the link to the internal wiki with a link to an external wiki. diff --git a/doc/ci/environments/index.md b/doc/ci/environments/index.md index 816db768dbc..896793a8ac1 100644 --- a/doc/ci/environments/index.md +++ b/doc/ci/environments/index.md @@ -811,6 +811,31 @@ stopped environment: Environments can also be deleted by using the [Environments API](../../api/environments.md#delete-an-environment). +### Prepare an environment + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/208655) in GitLab 13.2. + +By default, GitLab creates a [deployment](#viewing-deployment-history) every time a +build with the specified environment runs. Newer deployments can also +[cancel older ones](deployment_safety.md#skip-outdated-deployment-jobs). + +You may want to specify an environment keyword to +[protect builds from unauthorized access](protected_environments.md), or to get +access to [scoped variables](#scoping-environments-with-specs). In these cases, +you can use the `action: prepare` keyword to ensure deployments won't be created, +and no builds would be canceled: + +```yaml +build: + stage: build + script: + - echo "Building the app" + environment: + name: staging + action: prepare + url: https://staging.example.com +``` + ### Grouping similar environments > [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/7015) in GitLab 8.14. diff --git a/doc/development/performance.md b/doc/development/performance.md index 2841a7c339a..b33fc8b246f 100644 --- a/doc/development/performance.md +++ b/doc/development/performance.md @@ -173,11 +173,30 @@ dot -Tsvg project_policy_spec.dot > project_policy_spec.svg To load the profile in [kcachegrind](https://kcachegrind.github.io/): ```shell -stackprof tmp/project_policy_spec.dump --callgrind > project_policy_spec.callgrind +stackprof tmp/project_policy_spec.rb.dump --callgrind > project_policy_spec.callgrind kcachegrind project_policy_spec.callgrind # Linux qcachegrind project_policy_spec.callgrind # Mac ``` +For flamegraphs, enable raw collection first. Note that raw +collection can generate a very large file, so increase the `INTERVAL`, or +run on a smaller number of specs for smaller file size: + +```shell +RAW=true bin/rspec-stackprof spec/policies/group_member_policy_spec.rb +``` + +You can then generate, and view the resultant flamegraph. It might take a +while to generate based on the output file size: + +```shell +# Generate +stackprof --flamegraph tmp/group_member_policy_spec.rb.dump > group_member_policy_spec.flame + +# View +stackprof --flamegraph-viewer=group_member_policy_spec.flame +``` + It may be useful to zoom in on a specific method, for example: ```shell diff --git a/doc/user/project/integrations/overview.md b/doc/user/project/integrations/overview.md index 98ee5f9f641..9867af8976a 100644 --- a/doc/user/project/integrations/overview.md +++ b/doc/user/project/integrations/overview.md @@ -28,7 +28,7 @@ Click on the service links to see further configuration instructions and details | Buildkite | Continuous integration and deployments | Yes | | [Bugzilla](bugzilla.md) | Bugzilla issue tracker | No | | Campfire | Simple web-based real-time group chat | No | -| Confluence | Replaces the link to the internal wiki with a link to a Confluence Cloud Workspace. Service is behind a feature flag, disabled by default ([see details](../../../api/services.md#enable-or-disable-the-confluence-service-core-only)). | No | +| [Confluence](../../../api/services.md#confluence-service) | Replaces the link to the internal wiki with a link to a Confluence Cloud Workspace | No | | Custom Issue Tracker | Custom issue tracker | No | | [Discord Notifications](discord_notifications.md) | Receive event notifications in Discord | No | | Drone CI | Continuous Integration platform built on Docker, written in Go | Yes | diff --git a/lib/gitlab/cache/ci/project_pipeline_status.rb b/lib/gitlab/cache/ci/project_pipeline_status.rb index e7a7d23ef7e..d981f263c5e 100644 --- a/lib/gitlab/cache/ci/project_pipeline_status.rb +++ b/lib/gitlab/cache/ci/project_pipeline_status.rb @@ -49,7 +49,8 @@ module Gitlab def load_status return if loaded? - return unless commit + + return unless Gitlab::Ci::Features.pipeline_status_omit_commit_sha_in_cache_key?(project) || commit if has_cache? load_from_cache @@ -66,6 +67,8 @@ module Gitlab end def load_from_project + return unless commit + self.sha, self.status, self.ref = commit.sha, commit.status, project.default_branch end @@ -114,7 +117,11 @@ module Gitlab end def cache_key - "#{Gitlab::Redis::Cache::CACHE_NAMESPACE}:project:#{project.id}:pipeline_status:#{commit&.sha}" + if Gitlab::Ci::Features.pipeline_status_omit_commit_sha_in_cache_key?(project) + "#{Gitlab::Redis::Cache::CACHE_NAMESPACE}:project:#{project.id}:pipeline_status" + else + "#{Gitlab::Redis::Cache::CACHE_NAMESPACE}:project:#{project.id}:pipeline_status:#{commit&.sha}" + end end def commit diff --git a/lib/gitlab/ci/features.rb b/lib/gitlab/ci/features.rb index 554c30fadc8..5593bdaa723 100644 --- a/lib/gitlab/ci/features.rb +++ b/lib/gitlab/ci/features.rb @@ -34,6 +34,10 @@ module Gitlab ::Feature.enabled?(:ci_pipeline_latest, default_enabled: true) end + def self.pipeline_status_omit_commit_sha_in_cache_key?(project) + Feature.enabled?(:ci_pipeline_status_omit_commit_sha_in_cache_key, project) + end + def self.release_generation_enabled? ::Feature.enabled?(:ci_release_generation) end @@ -61,6 +65,10 @@ module Gitlab def self.destroy_only_unlocked_expired_artifacts_enabled? ::Feature.enabled?(:destroy_only_unlocked_expired_artifacts, default_enabled: false) end + + def self.bulk_insert_on_create?(project) + ::Feature.enabled?(:ci_bulk_insert_on_create, project, default_enabled: true) + end end end end diff --git a/lib/gitlab/ci/pipeline/chain/command.rb b/lib/gitlab/ci/pipeline/chain/command.rb index f36f199ab77..74b28b181bc 100644 --- a/lib/gitlab/ci/pipeline/chain/command.rb +++ b/lib/gitlab/ci/pipeline/chain/command.rb @@ -78,7 +78,7 @@ module Gitlab end def metrics - @metrics ||= Chain::Metrics.new + @metrics ||= ::Gitlab::Ci::Pipeline::Metrics.new end def observe_creation_duration(duration) diff --git a/lib/gitlab/ci/pipeline/chain/create.rb b/lib/gitlab/ci/pipeline/chain/create.rb index aa627bdb009..34649fe16f3 100644 --- a/lib/gitlab/ci/pipeline/chain/create.rb +++ b/lib/gitlab/ci/pipeline/chain/create.rb @@ -8,7 +8,9 @@ module Gitlab include Chain::Helpers def perform! - pipeline.save! + BulkInsertableAssociations.with_bulk_insert(enabled: ::Gitlab::Ci::Features.bulk_insert_on_create?(project)) do + pipeline.save! + end rescue ActiveRecord::RecordInvalid => e error("Failed to persist the pipeline: #{e}") end diff --git a/lib/gitlab/ci/pipeline/chain/metrics.rb b/lib/gitlab/ci/pipeline/chain/metrics.rb deleted file mode 100644 index 980ab2de9b0..00000000000 --- a/lib/gitlab/ci/pipeline/chain/metrics.rb +++ /dev/null @@ -1,35 +0,0 @@ -# frozen_string_literal: true - -module Gitlab - module Ci - module Pipeline - module Chain - class Metrics - include Gitlab::Utils::StrongMemoize - - def pipeline_creation_duration_histogram - strong_memoize(:pipeline_creation_duration_histogram) do - name = :gitlab_ci_pipeline_creation_duration_seconds - comment = 'Pipeline creation duration' - labels = {} - buckets = [0.01, 0.05, 0.1, 0.5, 1.0, 2.0, 5.0, 20.0, 50.0, 240.0] - - ::Gitlab::Metrics.histogram(name, comment, labels, buckets) - end - end - - def pipeline_size_histogram - strong_memoize(:pipeline_size_histogram) do - name = :gitlab_ci_pipeline_size_builds - comment = 'Pipeline size' - labels = { source: nil } - buckets = [0, 1, 5, 10, 20, 50, 100, 200, 500, 1000] - - ::Gitlab::Metrics.histogram(name, comment, labels, buckets) - end - end - end - end - end - end -end diff --git a/lib/gitlab/ci/pipeline/metrics.rb b/lib/gitlab/ci/pipeline/metrics.rb new file mode 100644 index 00000000000..649da745eea --- /dev/null +++ b/lib/gitlab/ci/pipeline/metrics.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Pipeline + class Metrics + include Gitlab::Utils::StrongMemoize + + def pipeline_creation_duration_histogram + strong_memoize(:pipeline_creation_duration_histogram) do + name = :gitlab_ci_pipeline_creation_duration_seconds + comment = 'Pipeline creation duration' + labels = {} + buckets = [0.01, 0.05, 0.1, 0.5, 1.0, 2.0, 5.0, 20.0, 50.0, 240.0] + + ::Gitlab::Metrics.histogram(name, comment, labels, buckets) + end + end + + def pipeline_size_histogram + strong_memoize(:pipeline_size_histogram) do + name = :gitlab_ci_pipeline_size_builds + comment = 'Pipeline size' + labels = { source: nil } + buckets = [0, 1, 5, 10, 20, 50, 100, 200, 500, 1000] + + ::Gitlab::Metrics.histogram(name, comment, labels, buckets) + end + end + + def pipeline_processing_events_counter + strong_memoize(:pipeline_processing_events_counter) do + name = :gitlab_ci_pipeline_processing_events_total + comment = 'Total amount of pipeline processing events' + + Gitlab::Metrics.counter(name, comment) + end + end + end + end + end +end diff --git a/lib/gitlab/ci/templates/Android-Fastlane.gitlab-ci.yml b/lib/gitlab/ci/templates/Android-Fastlane.gitlab-ci.yml index be584814271..5ebbbf15682 100644 --- a/lib/gitlab/ci/templates/Android-Fastlane.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Android-Fastlane.gitlab-ci.yml @@ -20,7 +20,7 @@ stages: - docker:dind script: - docker login -u gitlab-ci-token -p $CI_JOB_TOKEN $CI_REGISTRY - - docker pull $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG || true + - docker pull --quiet $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG || true - docker build --cache-from $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG -t $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG . - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG diff --git a/lib/gitlab/ci/templates/Security/Secure-Binaries.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/Secure-Binaries.gitlab-ci.yml index b6c05c61db1..d6cc446b9fc 100644 --- a/lib/gitlab/ci/templates/Security/Secure-Binaries.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Security/Secure-Binaries.gitlab-ci.yml @@ -40,7 +40,7 @@ variables: - docker info - env - if [ -z "$SECURE_BINARIES_IMAGE" ]; then export SECURE_BINARIES_IMAGE=${SECURE_BINARIES_IMAGE:-"registry.gitlab.com/gitlab-org/security-products/analyzers/${CI_JOB_NAME}:${SECURE_BINARIES_ANALYZER_VERSION}"}; fi - - docker pull ${SECURE_BINARIES_IMAGE} + - docker pull --quiet ${SECURE_BINARIES_IMAGE} - mkdir -p output/$(dirname ${CI_JOB_NAME}) - | if [ "$SECURE_BINARIES_SAVE_ARTIFACTS" = "true" ]; then diff --git a/lib/gitlab/import_export/project/import_export.yml b/lib/gitlab/import_export/project/import_export.yml index e3126719aea..aa961bd8d19 100644 --- a/lib/gitlab/import_export/project/import_export.yml +++ b/lib/gitlab/import_export/project/import_export.yml @@ -316,6 +316,7 @@ excluded_attributes: - :protected_branch_id push_access_levels: - :protected_branch_id + - :deploy_key_id unprotect_access_levels: - :protected_branch_id create_access_levels: diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 5303a8106e4..1af227968e5 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -28508,9 +28508,6 @@ msgstr[1] "" msgid "reset it." msgstr "" -msgid "resolved the corresponding error and closed the issue." -msgstr "" - msgid "revised" msgstr "" diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb index b86f2b24faa..c02632c2c60 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require 'securerandom' + module QA RSpec.describe 'Create' do describe 'File templates' do @@ -54,12 +56,14 @@ module QA expect(form).to have_normalized_ws_text(content[0..100]) + form.add_name("#{SecureRandom.hex(8)}/#{template[:file_name]}") form.commit_changes - expect(form).to have_content('The file has been successfully created.') - expect(form).to have_content(template[:file_name]) - expect(form).to have_content('Add new file') - expect(form).to have_normalized_ws_text(content[0..100]) + aggregate_failures "indications of file created" do + expect(form).to have_content(template[:file_name]) + expect(form).to have_normalized_ws_text(content[0..100]) + expect(form).to have_content('Add new file') + end end end end diff --git a/spec/controllers/autocomplete_controller_spec.rb b/spec/controllers/autocomplete_controller_spec.rb index aeb3f4dcb17..e7c0bc43e86 100644 --- a/spec/controllers/autocomplete_controller_spec.rb +++ b/spec/controllers/autocomplete_controller_spec.rb @@ -365,6 +365,56 @@ RSpec.describe AutocompleteController do end end + context 'GET deploy_keys_with_owners' do + let!(:deploy_key) { create(:deploy_key, user: user) } + let!(:deploy_keys_project) { create(:deploy_keys_project, :write_access, project: project, deploy_key: deploy_key) } + + context 'unauthorized user' do + it 'returns a not found response' do + get(:deploy_keys_with_owners, params: { project_id: project.id }) + + expect(response).to have_gitlab_http_status(:redirect) + end + end + + context 'when the user who can read the project is logged in' do + before do + sign_in(user) + end + + it 'renders the deploy key in a json payload, with its owner' do + get(:deploy_keys_with_owners, params: { project_id: project.id }) + + expect(json_response.count).to eq(1) + expect(json_response.first['title']).to eq(deploy_key.title) + expect(json_response.first['owner']['id']).to eq(deploy_key.user.id) + end + + context 'with an unknown project' do + it 'returns a not found response' do + get(:deploy_keys_with_owners, params: { project_id: 9999 }) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'and the user cannot read the owner of the key' do + before do + allow(Ability).to receive(:allowed?).and_call_original + allow(Ability).to receive(:allowed?).with(user, :read_user, deploy_key.user).and_return(false) + end + + it 'returns a payload without owner' do + get(:deploy_keys_with_owners, params: { project_id: project.id }) + + expect(json_response.count).to eq(1) + expect(json_response.first['title']).to eq(deploy_key.title) + expect(json_response.first['owner']).to be_nil + end + end + end + end + context 'Get merge_request_target_branches' do let!(:merge_request) { create(:merge_request, source_project: project, target_branch: 'feature') } diff --git a/spec/frontend/issuables_list/components/issuable_spec.js b/spec/frontend/issuables_list/components/issuable_spec.js index 78a38506059..87868b7eeff 100644 --- a/spec/frontend/issuables_list/components/issuable_spec.js +++ b/spec/frontend/issuables_list/components/issuable_spec.js @@ -80,6 +80,7 @@ describe('Issuable component', () => { wrapper.findAll(GlIcon).wrappers.some(iconWrapper => iconWrapper.props('name') === 'eye-slash'); const findTaskStatus = () => wrapper.find('.task-status'); const findOpenedAgoContainer = () => wrapper.find('[data-testid="openedByMessage"]'); + const findAuthor = () => wrapper.find({ ref: 'openedAgoByContainer' }); const findMilestone = () => wrapper.find('.js-milestone'); const findMilestoneTooltip = () => findMilestone().attributes('title'); const findDueDate = () => wrapper.find('.js-due-date'); @@ -94,6 +95,7 @@ describe('Issuable component', () => { const findScopedLabels = () => findLabels().filter(w => isScopedLabel({ title: w.text() })); const findUnscopedLabels = () => findLabels().filter(w => !isScopedLabel({ title: w.text() })); const findIssuableTitle = () => wrapper.find('[data-testid="issuable-title"]'); + const findIssuableStatus = () => wrapper.find('[data-testid="issuable-status"]'); const containsJiraLogo = () => wrapper.contains('[data-testid="jira-logo"]'); describe('when mounted', () => { @@ -235,6 +237,24 @@ describe('Issuable component', () => { it('opens issuable in a new tab', () => { expect(findIssuableTitle().props('target')).toBe('_blank'); }); + + it('opens author in a new tab', () => { + expect(findAuthor().props('target')).toBe('_blank'); + }); + + describe('with Jira status', () => { + const expectedStatus = 'In Progress'; + + beforeEach(() => { + issuable.status = expectedStatus; + + factory({ issuable }); + }); + + it('renders the Jira status', () => { + expect(findIssuableStatus().text()).toBe(expectedStatus); + }); + }); }); describe('with task status', () => { diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js index 92ef20aad6c..05508d14209 100644 --- a/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js +++ b/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js @@ -139,14 +139,6 @@ describe('FilteredSearchBarRoot', () => { }); }); - describe('getRecentSearches', () => { - it('returns array of strings representing recent searches', () => { - wrapper.vm.recentSearchesStore.setRecentSearches(['foo']); - - expect(wrapper.vm.getRecentSearches()).toEqual(['foo']); - }); - }); - describe('handleSortOptionClick', () => { it('emits component event `onSort` with selected sort by value', () => { wrapper.vm.handleSortOptionClick(mockSortOptions[1]); @@ -178,6 +170,14 @@ describe('FilteredSearchBarRoot', () => { }); }); + describe('handleHistoryItemSelected', () => { + it('emits `onFilter` event with provided filters param', () => { + wrapper.vm.handleHistoryItemSelected(mockHistoryItems[0]); + + expect(wrapper.emitted('onFilter')[0]).toEqual([mockHistoryItems[0]]); + }); + }); + describe('handleClearHistory', () => { it('clears search history from recent searches store', () => { jest.spyOn(wrapper.vm.recentSearchesStore, 'setRecentSearches').mockReturnValue([]); @@ -187,7 +187,7 @@ describe('FilteredSearchBarRoot', () => { expect(wrapper.vm.recentSearchesStore.setRecentSearches).toHaveBeenCalledWith([]); expect(wrapper.vm.recentSearchesService.save).toHaveBeenCalledWith([]); - expect(wrapper.vm.getRecentSearches()).toEqual([]); + expect(wrapper.vm.recentSearches).toEqual([]); }); }); @@ -223,6 +223,16 @@ describe('FilteredSearchBarRoot', () => { }); }); + it('sets `recentSearches` data prop with array of searches', () => { + jest.spyOn(wrapper.vm.recentSearchesService, 'save'); + + wrapper.vm.handleFilterSubmit(mockFilters); + + return wrapper.vm.recentSearchesPromise.then(() => { + expect(wrapper.vm.recentSearches).toEqual([mockFilters]); + }); + }); + it('emits component event `onFilter` with provided filters param', () => { wrapper.vm.handleFilterSubmit(mockFilters); @@ -236,10 +246,9 @@ describe('FilteredSearchBarRoot', () => { wrapper.setData({ selectedSortOption: mockSortOptions[0], selectedSortDirection: SortDirection.descending, + recentSearches: mockHistoryItems, }); - wrapper.vm.recentSearchesStore.setRecentSearches(mockHistoryItems); - return wrapper.vm.$nextTick(); }); diff --git a/spec/graphql/types/tree/blob_type_spec.rb b/spec/graphql/types/tree/blob_type_spec.rb index 2c9089de3dd..73d61d4860c 100644 --- a/spec/graphql/types/tree/blob_type_spec.rb +++ b/spec/graphql/types/tree/blob_type_spec.rb @@ -5,5 +5,5 @@ require 'spec_helper' RSpec.describe Types::Tree::BlobType do specify { expect(described_class.graphql_name).to eq('Blob') } - specify { expect(described_class).to have_graphql_fields(:id, :sha, :name, :type, :path, :flat_path, :web_url, :lfs_oid) } + specify { expect(described_class).to have_graphql_fields(:id, :sha, :name, :type, :path, :flat_path, :web_url, :lfs_oid, :mode) } end diff --git a/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb b/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb index beeccdf40a1..8d625cab1d8 100644 --- a/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb +++ b/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe Gitlab::Cache::Ci::ProjectPipelineStatus, :clean_gitlab_redis_cache do - let!(:project) { create(:project, :repository) } + let_it_be(:project) { create(:project, :repository) } let(:pipeline_status) { described_class.new(project) } let(:cache_key) { pipeline_status.cache_key } @@ -77,6 +77,62 @@ RSpec.describe Gitlab::Cache::Ci::ProjectPipelineStatus, :clean_gitlab_redis_cac end describe '#load_status' do + describe 'gitaly call counts', :request_store do + context 'not cached' do + before do + expect(pipeline_status).not_to be_has_cache + end + + context 'ci_pipeline_status_omit_commit_sha_in_cache_key is enabled' do + before do + stub_feature_flags(ci_pipeline_status_omit_commit_sha_in_cache_key: project) + end + + it 'makes a Gitaly call' do + expect { pipeline_status.load_status }.to change { Gitlab::GitalyClient.get_request_count }.by(1) + end + end + + context 'ci_pipeline_status_omit_commit_sha_in_cache_key is disabled' do + before do + stub_feature_flags(ci_pipeline_status_omit_commit_sha_in_cache_key: false) + end + + it 'makes a Gitaly calls' do + expect { pipeline_status.load_status }.to change { Gitlab::GitalyClient.get_request_count }.by(1) + end + end + end + + context 'cached' do + before do + described_class.load_in_batch_for_projects([project]) + + expect(pipeline_status).to be_has_cache + end + + context 'ci_pipeline_status_omit_commit_sha_in_cache_key is enabled' do + before do + stub_feature_flags(ci_pipeline_status_omit_commit_sha_in_cache_key: project) + end + + it 'makes no Gitaly calls' do + expect { pipeline_status.load_status }.to change { Gitlab::GitalyClient.get_request_count }.by(0) + end + end + + context 'ci_pipeline_status_omit_commit_sha_in_cache_key is disabled' do + before do + stub_feature_flags(ci_pipeline_status_omit_commit_sha_in_cache_key: false) + end + + it 'makes a Gitaly calls' do + expect { pipeline_status.load_status }.to change { Gitlab::GitalyClient.get_request_count }.by(1) + end + end + end + end + it 'loads the status from the cache when there is one' do expect(pipeline_status).to receive(:has_cache?).and_return(true) expect(pipeline_status).to receive(:load_from_cache) diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index 9707c0a4ff4..2d313b4dcad 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -588,6 +588,7 @@ ProtectedBranch::PushAccessLevel: - updated_at - user_id - group_id +- deploy_key_id ProtectedBranch::UnprotectAccessLevel: - id - protected_branch_id diff --git a/spec/models/ci/build_need_spec.rb b/spec/models/ci/build_need_spec.rb index d36a3768518..43cce073918 100644 --- a/spec/models/ci/build_need_spec.rb +++ b/spec/models/ci/build_need_spec.rb @@ -17,4 +17,22 @@ RSpec.describe Ci::BuildNeed, model: true do it { expect(described_class.artifacts).to contain_exactly(with_artifacts) } end + + describe 'BulkInsertSafe' do + let(:ci_build) { build(:ci_build) } + + it "bulk inserts from Ci::Build model" do + ci_build.needs_attributes = [ + { name: "build", artifacts: true }, + { name: "build2", artifacts: true }, + { name: "build3", artifacts: true } + ] + + expect(described_class).to receive(:bulk_insert!).and_call_original + + BulkInsertableAssociations.with_bulk_insert do + ci_build.save! + end + end + end end diff --git a/spec/models/deploy_keys_project_spec.rb b/spec/models/deploy_keys_project_spec.rb index ccc2c64e02c..7dd4d3129de 100644 --- a/spec/models/deploy_keys_project_spec.rb +++ b/spec/models/deploy_keys_project_spec.rb @@ -13,6 +13,21 @@ RSpec.describe DeployKeysProject do it { is_expected.to validate_presence_of(:deploy_key) } end + describe '.with_deploy_keys' do + subject(:scoped_query) { described_class.with_deploy_keys.last } + + it 'includes deploy_keys in query' do + project = create(:project) + create(:deploy_keys_project, project: project, deploy_key: create(:deploy_key)) + + includes_query_count = ActiveRecord::QueryRecorder.new { scoped_query }.count + deploy_key_query_count = ActiveRecord::QueryRecorder.new { scoped_query.deploy_key }.count + + expect(includes_query_count).to eq(2) + expect(deploy_key_query_count).to eq(0) + end + end + describe "Destroying" do let(:project) { create(:project) } subject { create(:deploy_keys_project, project: project) } diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb index 8988f5d8703..d153ccedf8c 100644 --- a/spec/models/merge_request_diff_spec.rb +++ b/spec/models/merge_request_diff_spec.rb @@ -162,7 +162,8 @@ RSpec.describe MergeRequestDiff do let(:uploader) { ExternalDiffUploader } let(:file_store) { uploader::Store::LOCAL } let(:remote_store) { uploader::Store::REMOTE } - let(:diff) { create(:merge_request).merge_request_diff } + let(:merge_request) { create(:merge_request) } + let(:diff) { merge_request.merge_request_diff } it 'converts from in-database to external file storage' do expect(diff).not_to be_stored_externally @@ -233,6 +234,33 @@ RSpec.describe MergeRequestDiff do diff.migrate_files_to_external_storage! end + + context 'diff adds an empty file' do + let(:project) { create(:project, :test_repo) } + let(:merge_request) do + create( + :merge_request, + source_project: project, + target_project: project, + source_branch: 'empty-file', + target_branch: 'master' + ) + end + + it 'migrates the diff to object storage' do + create_file_in_repo(project, 'master', 'empty-file', 'empty-file', '') + + expect(diff).not_to be_stored_externally + + stub_external_diffs_setting(enabled: true) + stub_external_diffs_object_storage(uploader, direct_upload: true) + + diff.migrate_files_to_external_storage! + + expect(diff).to be_stored_externally + expect(diff.external_diff_store).to eq(remote_store) + end + end end describe '#migrate_files_to_database!' do @@ -500,7 +528,7 @@ RSpec.describe MergeRequestDiff do include_examples 'merge request diffs' end - describe 'external diffs always enabled' do + describe 'external diffs on disk always enabled' do before do stub_external_diffs_setting(enabled: true, when: 'always') end @@ -508,6 +536,63 @@ RSpec.describe MergeRequestDiff do include_examples 'merge request diffs' end + describe 'external diffs in object storage always enabled' do + let(:uploader) { ExternalDiffUploader } + let(:remote_store) { uploader::Store::REMOTE } + + subject(:diff) { merge_request.merge_request_diff } + + before do + stub_external_diffs_setting(enabled: true, when: 'always') + stub_external_diffs_object_storage(uploader, direct_upload: true) + end + + # We can't use the full merge request diffs shared examples here because + # reading from the fake object store isn't implemented yet + + context 'empty diff' do + let(:merge_request) { create(:merge_request, :without_diffs) } + + it 'creates an empty diff' do + expect(diff.state).to eq('empty') + expect(diff).not_to be_stored_externally + end + end + + context 'normal diff' do + let(:merge_request) { create(:merge_request) } + + it 'creates a diff in object storage' do + expect(diff).to be_stored_externally + expect(diff.state).to eq('collected') + expect(diff.external_diff_store).to eq(remote_store) + end + end + + context 'diff adding an empty file' do + let(:project) { create(:project, :test_repo) } + let(:merge_request) do + create( + :merge_request, + source_project: project, + target_project: project, + source_branch: 'empty-file', + target_branch: 'master' + ) + end + + it 'creates a diff in object storage' do + create_file_in_repo(project, 'master', 'empty-file', 'empty-file', '') + + diff.reload + + expect(diff).to be_stored_externally + expect(diff.state).to eq('collected') + expect(diff.external_diff_store).to eq(remote_store) + end + end + end + describe 'exernal diffs enabled for outdated diffs' do before do stub_external_diffs_setting(enabled: true, when: 'outdated') diff --git a/spec/models/milestone_note_spec.rb b/spec/models/milestone_note_spec.rb index 058272dd5ec..db1a7ca05f8 100644 --- a/spec/models/milestone_note_spec.rb +++ b/spec/models/milestone_note_spec.rb @@ -11,9 +11,7 @@ RSpec.describe MilestoneNote do subject { described_class.from_event(event, resource: noteable, resource_parent: project) } - it_behaves_like 'a system note', exclude_project: true do - let(:action) { 'milestone' } - end + it_behaves_like 'a synthetic note', 'milestone' context 'with a remove milestone event' do let(:milestone) { create(:milestone) } diff --git a/spec/models/state_note_spec.rb b/spec/models/state_note_spec.rb index 5249c1be9ca..bd07af7ceca 100644 --- a/spec/models/state_note_spec.rb +++ b/spec/models/state_note_spec.rb @@ -10,18 +10,62 @@ RSpec.describe StateNote do ResourceStateEvent.states.each do |state, _value| context "with event state #{state}" do - let_it_be(:event) { create(:resource_state_event, issue: noteable, state: state, created_at: '2020-02-05') } + let(:event) { create(:resource_state_event, issue: noteable, state: state, created_at: '2020-02-05') } subject { described_class.from_event(event, resource: noteable, resource_parent: project) } - it_behaves_like 'a system note', exclude_project: true do - let(:action) { state.to_s } + it_behaves_like 'a synthetic note', state == 'reopened' ? 'opened' : state + + it 'contains the expected values' do + expect(subject.author).to eq(author) + expect(subject.created_at).to eq(event.created_at) + expect(subject.note).to eq(state) + end + end + end + + context 'with a mentionable source' do + subject { described_class.from_event(event, resource: noteable, resource_parent: project) } + + context 'with a commit' do + let(:commit) { create(:commit, project: project) } + let(:event) { create(:resource_state_event, issue: noteable, state: :closed, created_at: '2020-02-05', source_commit: commit.id) } + + it 'contains the expected values' do + expect(subject.author).to eq(author) + expect(subject.created_at).to eq(subject.created_at) + expect(subject.note).to eq("closed via commit #{commit.id}") + end + end + + context 'with a merge request' do + let(:merge_request) { create(:merge_request, source_project: project) } + let(:event) { create(:resource_state_event, issue: noteable, state: :closed, created_at: '2020-02-05', source_merge_request: merge_request) } + + it 'contains the expected values' do + expect(subject.author).to eq(author) + expect(subject.created_at).to eq(event.created_at) + expect(subject.note).to eq("closed via merge request !#{merge_request.iid}") + end + end + + context 'when closed by error tracking' do + let(:event) { create(:resource_state_event, issue: noteable, state: :closed, created_at: '2020-02-05', close_after_error_tracking_resolve: true) } + + it 'contains the expected values' do + expect(subject.author).to eq(author) + expect(subject.created_at).to eq(event.created_at) + expect(subject.note).to eq('resolved the corresponding error and closed the issue.') end + end + + context 'when closed by promotheus alert' do + let(:event) { create(:resource_state_event, issue: noteable, state: :closed, created_at: '2020-02-05', close_auto_resolve_prometheus_alert: true) } it 'contains the expected values' do expect(subject.author).to eq(author) expect(subject.created_at).to eq(event.created_at) - expect(subject.note_html).to eq("<p dir=\"auto\">#{state}</p>") + expect(subject.note).to eq('automatically closed this issue because the alert resolved.') end end end diff --git a/spec/serializers/deploy_key_entity_spec.rb b/spec/serializers/deploy_key_entity_spec.rb index 3354db5cf8d..3404d27a23c 100644 --- a/spec/serializers/deploy_key_entity_spec.rb +++ b/spec/serializers/deploy_key_entity_spec.rb @@ -9,8 +9,9 @@ RSpec.describe DeployKeyEntity do let(:project) { create(:project, :internal)} let(:project_private) { create(:project, :private)} let(:deploy_key) { create(:deploy_key) } + let(:options) { { user: user } } - let(:entity) { described_class.new(deploy_key, user: user) } + let(:entity) { described_class.new(deploy_key, options) } before do project.deploy_keys << deploy_key @@ -74,4 +75,42 @@ RSpec.describe DeployKeyEntity do it { expect(entity_public.as_json).to include(can_edit: true) } end end + + describe 'with_owner option' do + it 'does not return an owner payload when it is set to false' do + options[:with_owner] = false + + payload = entity.as_json + + expect(payload[:owner]).not_to be_present + end + + describe 'when with_owner is set to true' do + before do + options[:with_owner] = true + end + + it 'returns an owner payload' do + payload = entity.as_json + + expect(payload[:owner]).to be_present + expect(payload[:owner].keys).to include(:id, :name, :username, :avatar_url) + end + + it 'does not return an owner if current_user cannot read the owner' do + allow(Ability).to receive(:allowed?).and_call_original + allow(Ability).to receive(:allowed?).with(options[:user], :read_user, deploy_key.user).and_return(false) + + payload = entity.as_json + + expect(payload[:owner]).to be_nil + end + end + end + + it 'does not return an owner payload with_owner option not passed in' do + payload = entity.as_json + + expect(payload[:owner]).not_to be_present + end end diff --git a/spec/services/alert_management/process_prometheus_alert_service_spec.rb b/spec/services/alert_management/process_prometheus_alert_service_spec.rb index 3618ca20f73..0ce88f6b5b7 100644 --- a/spec/services/alert_management/process_prometheus_alert_service_spec.rb +++ b/spec/services/alert_management/process_prometheus_alert_service_spec.rb @@ -117,19 +117,29 @@ RSpec.describe AlertManagement::ProcessPrometheusAlertService do expect { execute }.to change { alert.reload.resolved? }.to(true) end - context 'existing issue' do - let!(:alert) { create(:alert_management_alert, :with_issue, project: project, fingerprint: parsed_alert.gitlab_fingerprint) } - - it 'closes the issue' do - issue = alert.issue - - expect { execute } - .to change { issue.reload.state } - .from('opened') - .to('closed') + [true, false].each do |state_tracking_enabled| + context 'existing issue' do + before do + stub_feature_flags(track_resource_state_change_events: state_tracking_enabled) + end + + let!(:alert) { create(:alert_management_alert, :with_issue, project: project, fingerprint: parsed_alert.gitlab_fingerprint) } + + it 'closes the issue' do + issue = alert.issue + + expect { execute } + .to change { issue.reload.state } + .from('opened') + .to('closed') + end + + if state_tracking_enabled + specify { expect { execute }.to change(ResourceStateEvent, :count).by(1) } + else + specify { expect { execute }.to change(Note, :count).by(1) } + end end - - specify { expect { execute }.to change(Note, :count).by(1) } end end diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb index b8d39f49224..9dc518be996 100644 --- a/spec/services/ci/create_pipeline_service_spec.rb +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -80,7 +80,7 @@ RSpec.describe Ci::CreatePipelineService do it 'records pipeline size in a prometheus histogram' do histogram = spy('pipeline size histogram') - allow(Gitlab::Ci::Pipeline::Chain::Metrics) + allow(Gitlab::Ci::Pipeline::Metrics) .to receive(:new).and_return(histogram) execute_service @@ -1684,6 +1684,12 @@ RSpec.describe Ci::CreatePipelineService do expect(pipeline).to be_persisted expect(pipeline.builds.pluck(:name)).to contain_exactly("build_a", "test_a") end + + it 'bulk inserts all needs' do + expect(Ci::BuildNeed).to receive(:bulk_insert!).and_call_original + + expect(pipeline).to be_persisted + end end context 'when pipeline on feature is created' do diff --git a/spec/services/ci/process_pipeline_service_spec.rb b/spec/services/ci/process_pipeline_service_spec.rb index 6ebb3188f00..a7889f0644d 100644 --- a/spec/services/ci/process_pipeline_service_spec.rb +++ b/spec/services/ci/process_pipeline_service_spec.rb @@ -10,38 +10,52 @@ RSpec.describe Ci::ProcessPipelineService do create(:ci_empty_pipeline, ref: 'master', project: project) end + subject { described_class.new(pipeline) } + before do stub_ci_pipeline_to_return_yaml_file - stub_not_protect_default_branch project.add_developer(user) end - context 'updates a list of retried builds' do - subject { described_class.retried.order(:id) } + describe 'processing events counter' do + let(:metrics) { double('pipeline metrics') } + let(:counter) { double('events counter') } + + before do + allow(subject) + .to receive(:metrics).and_return(metrics) + allow(metrics) + .to receive(:pipeline_processing_events_counter) + .and_return(counter) + end + + it 'increments processing events counter' do + expect(counter).to receive(:increment) + + subject.execute + end + end + describe 'updating a list of retried builds' do let!(:build_retried) { create_build('build') } let!(:build) { create_build('build') } let!(:test) { create_build('test') } it 'returns unique statuses' do - process_pipeline + subject.execute expect(all_builds.latest).to contain_exactly(build, test) expect(all_builds.retried).to contain_exactly(build_retried) end - end - - def process_pipeline - described_class.new(pipeline).execute - end - def create_build(name, **opts) - create(:ci_build, :created, pipeline: pipeline, name: name, **opts) - end + def create_build(name, **opts) + create(:ci_build, :created, pipeline: pipeline, name: name, **opts) + end - def all_builds - pipeline.builds.order(:stage_idx, :id) + def all_builds + pipeline.builds.order(:stage_idx, :id) + end end end diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb index c637d8fbe5c..5a245415b32 100644 --- a/spec/services/ci/retry_build_service_spec.rb +++ b/spec/services/ci/retry_build_service_spec.rb @@ -278,6 +278,19 @@ RSpec.describe Ci::RetryBuildService do expect(new_build.metadata.expanded_environment_name).to eq('production') end end + + context 'when build has needs' do + before do + create(:ci_build_need, build: build, name: 'build1') + create(:ci_build_need, build: build, name: 'build2') + end + + it 'bulk inserts all needs' do + expect(Ci::BuildNeed).to receive(:bulk_insert!).and_call_original + + new_build + end + end end context 'when user does not have ability to execute build' do diff --git a/spec/services/deploy_keys/collect_keys_service_spec.rb b/spec/services/deploy_keys/collect_keys_service_spec.rb new file mode 100644 index 00000000000..3442e5e456a --- /dev/null +++ b/spec/services/deploy_keys/collect_keys_service_spec.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe DeployKeys::CollectKeysService do + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project, :private) } + + subject { DeployKeys::CollectKeysService.new(project, user) } + + before do + project&.add_developer(user) + end + + context 'when no project is passed in' do + let(:project) { nil } + + it 'returns an empty Array' do + expect(subject.execute).to be_empty + end + end + + context 'when no user is passed in' do + let(:user) { nil } + + it 'returns an empty Array' do + expect(subject.execute).to be_empty + end + end + + context 'when a project is passed in' do + let_it_be(:deploy_keys_project) { create(:deploy_keys_project, :write_access, project: project) } + let_it_be(:deploy_key) { deploy_keys_project.deploy_key } + + it 'only returns deploy keys with write access' do + create(:deploy_keys_project, project: project) + + expect(subject.execute).to contain_exactly(deploy_key) + end + + it 'returns deploy keys only for this project' do + other_project = create(:project) + create(:deploy_keys_project, :write_access, project: other_project) + + expect(subject.execute).to contain_exactly(deploy_key) + end + end + + context 'when the user cannot read the project' do + before do + project.members.delete_all + end + + it 'returns an empty Array' do + expect(subject.execute).to be_empty + end + end +end diff --git a/spec/services/event_create_service_spec.rb b/spec/services/event_create_service_spec.rb index e9ed0493c21..2bcdda6d276 100644 --- a/spec/services/event_create_service_spec.rb +++ b/spec/services/event_create_service_spec.rb @@ -16,7 +16,6 @@ RSpec.describe EventCreateService do it "creates new event" do expect { service.open_issue(issue, issue.author) }.to change { Event.count } - expect { service.open_issue(issue, issue.author) }.to change { ResourceStateEvent.count } end end @@ -27,7 +26,6 @@ RSpec.describe EventCreateService do it "creates new event" do expect { service.close_issue(issue, issue.author) }.to change { Event.count } - expect { service.close_issue(issue, issue.author) }.to change { ResourceStateEvent.count } end end @@ -38,7 +36,6 @@ RSpec.describe EventCreateService do it "creates new event" do expect { service.reopen_issue(issue, issue.author) }.to change { Event.count } - expect { service.reopen_issue(issue, issue.author) }.to change { ResourceStateEvent.count } end end end @@ -51,7 +48,6 @@ RSpec.describe EventCreateService do it "creates new event" do expect { service.open_mr(merge_request, merge_request.author) }.to change { Event.count } - expect { service.open_mr(merge_request, merge_request.author) }.to change { ResourceStateEvent.count } end end @@ -62,7 +58,6 @@ RSpec.describe EventCreateService do it "creates new event" do expect { service.close_mr(merge_request, merge_request.author) }.to change { Event.count } - expect { service.close_mr(merge_request, merge_request.author) }.to change { ResourceStateEvent.count } end end @@ -73,7 +68,6 @@ RSpec.describe EventCreateService do it "creates new event" do expect { service.merge_mr(merge_request, merge_request.author) }.to change { Event.count } - expect { service.merge_mr(merge_request, merge_request.author) }.to change { ResourceStateEvent.count } end end @@ -84,7 +78,6 @@ RSpec.describe EventCreateService do it "creates new event" do expect { service.reopen_mr(merge_request, merge_request.author) }.to change { Event.count } - expect { service.reopen_mr(merge_request, merge_request.author) }.to change { ResourceStateEvent.count } end end diff --git a/spec/services/resource_events/change_state_service_spec.rb b/spec/services/resource_events/change_state_service_spec.rb index a2a2f119830..5b5379b241b 100644 --- a/spec/services/resource_events/change_state_service_spec.rb +++ b/spec/services/resource_events/change_state_service_spec.rb @@ -8,32 +8,89 @@ RSpec.describe ResourceEvents::ChangeStateService do let(:issue) { create(:issue, project: project) } let(:merge_request) { create(:merge_request, source_project: project) } + let(:source_commit) { create(:commit, project: project) } + let(:source_merge_request) { create(:merge_request, source_project: project, target_project: project, target_branch: 'foo') } - describe '#execute' do - context 'when resource is an issue' do - %w[opened reopened closed locked].each do |state| - it "creates the expected event if issue has #{state} state" do - described_class.new(user: user, resource: issue).execute(state) + shared_examples 'a state event' do + %w[opened reopened closed locked].each do |state| + it "creates the expected event if resource has #{state} state" do + described_class.new(user: user, resource: resource).execute(status: state, mentionable_source: source) + + event = resource.resource_state_events.last - event = issue.resource_state_events.last - expect(event.issue).to eq(issue) + if resource.is_a?(Issue) + expect(event.issue).to eq(resource) expect(event.merge_request).to be_nil - expect(event.state).to eq(state) + elsif resource.is_a?(MergeRequest) + expect(event.issue).to be_nil + expect(event.merge_request).to eq(resource) end + + expect(event.state).to eq(state) + + expect_event_source(event, source) end end + end - context 'when resource is a merge request' do - %w[opened reopened closed locked merged].each do |state| - it "creates the expected event if merge request has #{state} state" do - described_class.new(user: user, resource: merge_request).execute(state) + describe '#execute' do + context 'when resource is an Issue' do + context 'when no source is given' do + it_behaves_like 'a state event' do + let(:resource) { issue } + let(:source) { nil } + end + end - event = merge_request.resource_state_events.last - expect(event.issue).to be_nil - expect(event.merge_request).to eq(merge_request) - expect(event.state).to eq(state) + context 'when source commit is given' do + it_behaves_like 'a state event' do + let(:resource) { issue } + let(:source) { source_commit } + end + end + + context 'when source merge request is given' do + it_behaves_like 'a state event' do + let(:resource) { issue } + let(:source) { source_merge_request } end end end + + context 'when resource is a MergeRequest' do + context 'when no source is given' do + it_behaves_like 'a state event' do + let(:resource) { merge_request } + let(:source) { nil } + end + end + + context 'when source commit is given' do + it_behaves_like 'a state event' do + let(:resource) { merge_request } + let(:source) { source_commit } + end + end + + context 'when source merge request is given' do + it_behaves_like 'a state event' do + let(:resource) { merge_request } + let(:source) { source_merge_request } + end + end + end + end + + def expect_event_source(event, source) + if source.is_a?(MergeRequest) + expect(event.source_commit).to be_nil + expect(event.source_merge_request).to eq(source) + elsif source.is_a?(Commit) + expect(event.source_commit).to eq(source.id) + expect(event.source_merge_request).to be_nil + else + expect(event.source_merge_request).to be_nil + expect(event.source_commit).to be_nil + end end end diff --git a/spec/services/system_notes/issuables_service_spec.rb b/spec/services/system_notes/issuables_service_spec.rb index ca8b8ce4dcf..1b5b26d90da 100644 --- a/spec/services/system_notes/issuables_service_spec.rb +++ b/spec/services/system_notes/issuables_service_spec.rb @@ -161,7 +161,9 @@ RSpec.describe ::SystemNotes::IssuablesService do let(:status) { 'reopened' } let(:source) { nil } - it { is_expected.to be_nil } + it 'does not change note count' do + expect { subject }.not_to change { Note.count } + end end context 'with status reopened' do @@ -660,25 +662,67 @@ RSpec.describe ::SystemNotes::IssuablesService do describe '#close_after_error_tracking_resolve' do subject { service.close_after_error_tracking_resolve } - it_behaves_like 'a system note' do - let(:action) { 'closed' } + context 'when state tracking is enabled' do + before do + stub_feature_flags(track_resource_state_change_events: true) + end + + it 'creates the expected state event' do + subject + + event = ResourceStateEvent.last + + expect(event.close_after_error_tracking_resolve).to eq(true) + expect(event.state).to eq('closed') + end end - it 'creates the expected system note' do - expect(subject.note) + context 'when state tracking is disabled' do + before do + stub_feature_flags(track_resource_state_change_events: false) + end + + it_behaves_like 'a system note' do + let(:action) { 'closed' } + end + + it 'creates the expected system note' do + expect(subject.note) .to eq('resolved the corresponding error and closed the issue.') + end end end describe '#auto_resolve_prometheus_alert' do subject { service.auto_resolve_prometheus_alert } - it_behaves_like 'a system note' do - let(:action) { 'closed' } + context 'when state tracking is enabled' do + before do + stub_feature_flags(track_resource_state_change_events: true) + end + + it 'creates the expected state event' do + subject + + event = ResourceStateEvent.last + + expect(event.close_auto_resolve_prometheus_alert).to eq(true) + expect(event.state).to eq('closed') + end end - it 'creates the expected system note' do - expect(subject.note).to eq('automatically closed this issue because the alert resolved.') + context 'when state tracking is disabled' do + before do + stub_feature_flags(track_resource_state_change_events: false) + end + + it_behaves_like 'a system note' do + let(:action) { 'closed' } + end + + it 'creates the expected system note' do + expect(subject.note).to eq('automatically closed this issue because the alert resolved.') + end end end end diff --git a/spec/support/shared_examples/models/synthetic_note_shared_examples.rb b/spec/support/shared_examples/models/synthetic_note_shared_examples.rb new file mode 100644 index 00000000000..a41ade2950a --- /dev/null +++ b/spec/support/shared_examples/models/synthetic_note_shared_examples.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'a synthetic note' do |action| + it_behaves_like 'a system note', exclude_project: true do + let(:action) { action } + end + + describe '#discussion_id' do + before do + allow(event).to receive(:discussion_id).and_return('foobar42') + end + + it 'returns the expected discussion id' do + expect(subject.discussion_id(nil)).to eq('foobar42') + end + end +end |