Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
a898b6057e
commit
9215d9f761
2
Gemfile
2
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'
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -97,6 +97,9 @@ export default {
|
|||
isJiraIssue() {
|
||||
return this.issuable.external_tracker === 'jira';
|
||||
},
|
||||
linkTarget() {
|
||||
return this.isJiraIssue ? '_blank' : null;
|
||||
},
|
||||
issueCreatedToday() {
|
||||
return getDayDifference(new Date(this.issuable.created_at), new Date()) < 1;
|
||||
},
|
||||
|
@ -239,11 +242,7 @@ export default {
|
|||
:title="$options.confidentialTooltipText"
|
||||
:aria-label="$options.confidentialTooltipText"
|
||||
/>
|
||||
<gl-link
|
||||
:href="issuable.web_url"
|
||||
:target="isJiraIssue ? '_blank' : null"
|
||||
data-testid="issuable-title"
|
||||
>
|
||||
<gl-link :href="issuable.web_url" :target="linkTarget" data-testid="issuable-title">
|
||||
{{ issuable.title }}
|
||||
<gl-icon
|
||||
v-if="isJiraIssue"
|
||||
|
@ -281,6 +280,7 @@ export default {
|
|||
ref="openedAgoByContainer"
|
||||
v-bind="popoverDataAttrs"
|
||||
:href="issuableAuthor.web_url"
|
||||
:target="linkTarget"
|
||||
>
|
||||
{{ issuableAuthor.name }}
|
||||
</gl-link>
|
||||
|
@ -340,8 +340,8 @@ export default {
|
|||
<!-- Issuable meta -->
|
||||
<div class="flex-shrink-0 d-flex flex-column align-items-end justify-content-center">
|
||||
<div class="controls d-flex">
|
||||
<span v-if="isJiraIssue"> </span>
|
||||
<span v-if="isClosed" class="issuable-status">{{ __('CLOSED') }}</span>
|
||||
<span v-if="isJiraIssue" data-testid="issuable-status">{{ issuable.status }}</span>
|
||||
<span v-else-if="isClosed" class="issuable-status">{{ __('CLOSED') }}</span>
|
||||
|
||||
<issue-assignees
|
||||
:assignees="issuable.assignees"
|
||||
|
|
|
@ -45,6 +45,7 @@ query getFiles(
|
|||
edges {
|
||||
node {
|
||||
...TreeEntry
|
||||
mode
|
||||
webUrl
|
||||
lfsOid
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -6,6 +6,7 @@ class CommitStatus < ApplicationRecord
|
|||
include AfterCommitQueue
|
||||
include Presentable
|
||||
include EnumWithNil
|
||||
include BulkInsertableAssociations
|
||||
|
||||
self.table_name = 'ci_builds'
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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')
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Expose project deploy keys for autocompletion
|
||||
merge_request: 34875
|
||||
author:
|
||||
type: added
|
|
@ -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
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add source to resource state events
|
||||
merge_request: 32924
|
||||
author:
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Enable BulkInsertSafe on Ci::BuildNeed
|
||||
merge_request: 36815
|
||||
author:
|
||||
type: performance
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Remove need to call commit (gitaly call) in ProjectPipelineStatus
|
||||
merge_request: 33712
|
||||
author:
|
||||
type: performance
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Removes fixes that broke the pipeline table
|
||||
merge_request: 36803
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Suppress progress on docker pulling in builtin templates
|
||||
merge_request: 35253
|
||||
author: Takuya Noguchi
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Expose blob mode in GraphQL for repository files
|
||||
merge_request: 36488
|
||||
author:
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Update Rouge to v3.21.0
|
||||
merge_request: 36942
|
||||
author:
|
||||
type: other
|
|
@ -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'
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -793,6 +793,11 @@ type Blob implements Entry {
|
|||
"""
|
||||
lfsOid: String
|
||||
|
||||
"""
|
||||
Blob mode in numeric format
|
||||
"""
|
||||
mode: String
|
||||
|
||||
"""
|
||||
Name of the entry
|
||||
"""
|
||||
|
@ -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
|
||||
|
||||
|
|
|
@ -2056,6 +2056,20 @@
|
|||
"isDeprecated": false,
|
||||
"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",
|
||||
|
@ -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",
|
||||
|
|
|
@ -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 |
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 |
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -28508,9 +28508,6 @@ msgstr[1] ""
|
|||
msgid "reset it."
|
||||
msgstr ""
|
||||
|
||||
msgid "resolved the corresponding error and closed the issue."
|
||||
msgstr ""
|
||||
|
||||
msgid "revised"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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') }
|
||||
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -588,6 +588,7 @@ ProtectedBranch::PushAccessLevel:
|
|||
- updated_at
|
||||
- user_id
|
||||
- group_id
|
||||
- deploy_key_id
|
||||
ProtectedBranch::UnprotectAccessLevel:
|
||||
- id
|
||||
- protected_branch_id
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) }
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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) }
|
||||
|
|
|
@ -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 }
|
||||
end
|
||||
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_html).to eq("<p dir=\"auto\">#{state}</p>")
|
||||
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).to eq('automatically closed this issue because the alert resolved.')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) }
|
||||
[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
|
||||
|
||||
it 'closes the issue' do
|
||||
issue = alert.issue
|
||||
let!(:alert) { create(:alert_management_alert, :with_issue, project: project, fingerprint: parsed_alert.gitlab_fingerprint) }
|
||||
|
||||
expect { execute }
|
||||
.to change { issue.reload.state }
|
||||
.from('opened')
|
||||
.to('closed')
|
||||
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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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') }
|
||||
|
||||
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
|
||||
|
||||
if resource.is_a?(Issue)
|
||||
expect(event.issue).to eq(resource)
|
||||
expect(event.merge_request).to be_nil
|
||||
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
|
||||
|
||||
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)
|
||||
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 = issue.resource_state_events.last
|
||||
expect(event.issue).to eq(issue)
|
||||
expect(event.merge_request).to be_nil
|
||||
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 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)
|
||||
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
|
||||
|
||||
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) { 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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
Loading…
Reference in New Issue