Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.lock4
-rw-r--r--app/assets/javascripts/issuables_list/components/issuable.vue14
-rw-r--r--app/assets/javascripts/repository/queries/getFiles.query.graphql1
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue19
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss7
-rw-r--r--app/controllers/autocomplete_controller.rb16
-rw-r--r--app/graphql/types/tree/blob_type.rb2
-rw-r--r--app/models/audit_event.rb12
-rw-r--r--app/models/ci/build_need.rb2
-rw-r--r--app/models/commit_status.rb1
-rw-r--r--app/models/deploy_keys_project.rb1
-rw-r--r--app/models/resource_state_event.rb2
-rw-r--r--app/models/state_note.rb34
-rw-r--r--app/models/synthetic_note.rb18
-rw-r--r--app/serializers/deploy_key_entity.rb5
-rw-r--r--app/services/ci/process_pipeline_service.rb10
-rw-r--r--app/services/ci/retry_build_service.rb4
-rw-r--r--app/services/deploy_keys/collect_keys_service.rb27
-rw-r--r--app/services/event_create_service.rb26
-rw-r--r--app/services/resource_events/change_state_service.rb38
-rw-r--r--app/services/system_notes/issuables_service.rb25
-rw-r--r--app/views/admin/application_settings/repository.html.haml2
-rwxr-xr-xbin/rspec-stackprof3
-rw-r--r--changelogs/unreleased/30769-deploy-keys-push-protected-branches.yml5
-rw-r--r--changelogs/unreleased/branch-name-default-to-true.yml6
-rw-r--r--changelogs/unreleased/create-state-events-pd.yml5
-rw-r--r--changelogs/unreleased/enable-bulk-insert-for-needs.yml5
-rw-r--r--changelogs/unreleased/reduce_pipeline_status_gitaly_call.yml5
-rw-r--r--changelogs/unreleased/say-no-to-hacks.yml5
-rw-r--r--changelogs/unreleased/suppress-progress-on-pulling-image-in-builtin-templates.yml5
-rw-r--r--changelogs/unreleased/symlink-icon-graphql-file-mode.yml5
-rw-r--r--changelogs/unreleased/update-rouge-3-21.yml5
-rw-r--r--config/routes.rb1
-rw-r--r--db/migrate/20200524104346_add_source_to_resource_state_event.rb21
-rw-r--r--db/migrate/20200615141554_add_closed_by_fields_to_resource_state_events.rb17
-rw-r--r--db/migrate/20200617205000_add_deploy_key_id_to_push_access_levels.rb22
-rw-r--r--db/migrate/20200623073431_add_source_merge_request_id_to_resource_state_events.rb33
-rw-r--r--db/structure.sql22
-rw-r--r--doc/api/graphql/reference/gitlab_schema.graphql7
-rw-r--r--doc/api/graphql/reference/gitlab_schema.json16
-rw-r--r--doc/api/graphql/reference/index.md1
-rw-r--r--doc/api/services.md31
-rw-r--r--doc/ci/environments/index.md25
-rw-r--r--doc/development/performance.md21
-rw-r--r--doc/user/project/integrations/overview.md2
-rw-r--r--lib/gitlab/cache/ci/project_pipeline_status.rb11
-rw-r--r--lib/gitlab/ci/features.rb8
-rw-r--r--lib/gitlab/ci/pipeline/chain/command.rb2
-rw-r--r--lib/gitlab/ci/pipeline/chain/create.rb4
-rw-r--r--lib/gitlab/ci/pipeline/chain/metrics.rb35
-rw-r--r--lib/gitlab/ci/pipeline/metrics.rb42
-rw-r--r--lib/gitlab/ci/templates/Android-Fastlane.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Security/Secure-Binaries.gitlab-ci.yml2
-rw-r--r--lib/gitlab/import_export/project/import_export.yml1
-rw-r--r--locale/gitlab.pot3
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb12
-rw-r--r--spec/controllers/autocomplete_controller_spec.rb50
-rw-r--r--spec/frontend/issuables_list/components/issuable_spec.js20
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js31
-rw-r--r--spec/graphql/types/tree/blob_type_spec.rb2
-rw-r--r--spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb58
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml1
-rw-r--r--spec/models/ci/build_need_spec.rb18
-rw-r--r--spec/models/deploy_keys_project_spec.rb15
-rw-r--r--spec/models/merge_request_diff_spec.rb89
-rw-r--r--spec/models/milestone_note_spec.rb4
-rw-r--r--spec/models/state_note_spec.rb52
-rw-r--r--spec/serializers/deploy_key_entity_spec.rb41
-rw-r--r--spec/services/alert_management/process_prometheus_alert_service_spec.rb34
-rw-r--r--spec/services/ci/create_pipeline_service_spec.rb8
-rw-r--r--spec/services/ci/process_pipeline_service_spec.rb42
-rw-r--r--spec/services/ci/retry_build_service_spec.rb13
-rw-r--r--spec/services/deploy_keys/collect_keys_service_spec.rb58
-rw-r--r--spec/services/event_create_service_spec.rb7
-rw-r--r--spec/services/resource_events/change_state_service_spec.rb89
-rw-r--r--spec/services/system_notes/issuables_service_spec.rb62
-rw-r--r--spec/support/shared_examples/models/synthetic_note_shared_examples.rb17
78 files changed, 1130 insertions, 243 deletions
diff --git a/Gemfile b/Gemfile
index e69cf8c2dce..afe408fa99d 100644
--- a/Gemfile
+++ b/Gemfile
@@ -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">&nbsp;</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