Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-07-15 12:09:26 +00:00
parent a898b6057e
commit 9215d9f761
78 changed files with 1129 additions and 242 deletions

View File

@ -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'

View File

@ -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)

View File

@ -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"

View File

@ -45,6 +45,7 @@ query getFiles(
edges {
node {
...TreeEntry
mode
webUrl
lfsOid
}

View File

@ -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

View File

@ -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;

View File

@ -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

View File

@ -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

View File

@ -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')

View File

@ -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

View File

@ -6,6 +6,7 @@ class CommitStatus < ApplicationRecord
include AfterCommitQueue
include Presentable
include EnumWithNil
include BulkInsertableAssociations
self.table_name = 'ci_builds'

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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')

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,5 @@
---
title: Expose project deploy keys for autocompletion
merge_request: 34875
author:
type: added

View File

@ -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

View File

@ -0,0 +1,5 @@
---
title: Add source to resource state events
merge_request: 32924
author:
type: other

View File

@ -0,0 +1,5 @@
---
title: Enable BulkInsertSafe on Ci::BuildNeed
merge_request: 36815
author:
type: performance

View File

@ -0,0 +1,5 @@
---
title: Remove need to call commit (gitaly call) in ProjectPipelineStatus
merge_request: 33712
author:
type: performance

View File

@ -0,0 +1,5 @@
---
title: Removes fixes that broke the pipeline table
merge_request: 36803
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Suppress progress on docker pulling in builtin templates
merge_request: 35253
author: Takuya Noguchi
type: other

View File

@ -0,0 +1,5 @@
---
title: Expose blob mode in GraphQL for repository files
merge_request: 36488
author:
type: other

View File

@ -0,0 +1,5 @@
---
title: Update Rouge to v3.21.0
merge_request: 36942
author:
type: other

View File

@ -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'

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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",

View File

@ -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 |

View File

@ -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.

View File

@ -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.

View File

@ -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

View File

@ -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 |

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -28508,9 +28508,6 @@ msgstr[1] ""
msgid "reset it."
msgstr ""
msgid "resolved the corresponding error and closed the issue."
msgstr ""
msgid "revised"
msgstr ""

View File

@ -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

View File

@ -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') }

View File

@ -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', () => {

View File

@ -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();
});

View File

@ -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

View File

@ -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)

View File

@ -588,6 +588,7 @@ ProtectedBranch::PushAccessLevel:
- updated_at
- user_id
- group_id
- deploy_key_id
ProtectedBranch::UnprotectAccessLevel:
- id
- protected_branch_id

View File

@ -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

View File

@ -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) }

View File

@ -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')

View File

@ -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) }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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