Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-06-29 12:09:20 +00:00
parent 9ce66d4dcf
commit a0b26c6df5
81 changed files with 847 additions and 212 deletions

View File

@ -149,7 +149,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.19.0'
gem 'rouge', '~> 3.20.0'
gem 'truncato', '~> 0.7.11'
gem 'bootstrap_form', '~> 4.2.0'
gem 'nokogiri', '~> 1.10.9'

View File

@ -891,7 +891,7 @@ GEM
rexml (3.2.4)
rinku (2.0.0)
rotp (2.1.2)
rouge (3.19.0)
rouge (3.20.0)
rqrcode (0.7.0)
chunky_png
rqrcode-rails3 (0.1.7)
@ -1356,7 +1356,7 @@ DEPENDENCIES
request_store (~> 1.5)
responders (~> 3.0)
retriable (~> 3.1.2)
rouge (~> 3.19.0)
rouge (~> 3.20.0)
rqrcode-rails3 (~> 0.1.7)
rspec-parameterized
rspec-rails (~> 4.0.0)

View File

@ -32,7 +32,8 @@ import {
} from '../constants';
import AlertStatus from './alert_status.vue';
const tdClass = 'table-col gl-display-flex d-md-table-cell gl-align-items-center';
const tdClass =
'table-col gl-display-flex d-md-table-cell gl-align-items-center gl-white-space-nowrap';
const thClass = 'gl-hover-bg-blue-50';
const bodyTrClass =
'gl-border-1 gl-border-t-solid gl-border-gray-100 gl-hover-bg-blue-50 gl-hover-cursor-pointer gl-hover-border-b-solid gl-hover-border-blue-200';
@ -60,40 +61,34 @@ export default {
key: 'severity',
label: s__('AlertManagement|Severity'),
tdClass: `${tdClass} rounded-top text-capitalize`,
thClass,
thClass: `${thClass} gl-w-eighth`,
sortable: true,
},
{
key: 'startedAt',
label: s__('AlertManagement|Start time'),
thClass: `${thClass} js-started-at`,
tdClass,
sortable: true,
},
{
key: 'endedAt',
label: s__('AlertManagement|End time'),
thClass,
thClass: `${thClass} js-started-at w-15p`,
tdClass,
sortable: true,
},
{
key: 'title',
label: s__('AlertManagement|Alert'),
thClass: `${thClass} w-30p gl-pointer-events-none`,
thClass: `${thClass} gl-pointer-events-none`,
tdClass,
sortable: false,
},
{
key: 'eventCount',
label: s__('AlertManagement|Events'),
thClass: `${thClass} text-right gl-pr-9 w-3rem`,
thClass: `${thClass} text-right gl-w-12`,
tdClass: `${tdClass} text-md-right`,
sortable: true,
},
{
key: 'assignees',
label: s__('AlertManagement|Assignees'),
thClass: 'gl-w-eighth',
tdClass,
},
{
@ -377,6 +372,7 @@ export default {
:sort-desc.sync="sortDesc"
:sort-by.sync="sortBy"
sort-icon-left
fixed
@row-clicked="navigateToAlertDetails"
@sort-changed="fetchSortedData"
>
@ -399,16 +395,12 @@ export default {
<time-ago v-if="item.startedAt" :time="item.startedAt" />
</template>
<template #cell(endedAt)="{ item }">
<time-ago v-if="item.endedAt" :time="item.endedAt" />
</template>
<template #cell(eventCount)="{ item }">
{{ item.eventCount }}
</template>
<template #cell(title)="{ item }">
<div class="gl-max-w-full text-truncate">{{ item.title }}</div>
<div class="gl-max-w-full text-truncate" :title="item.title">{{ item.title }}</div>
</template>
<template #cell(assignees)="{ item }">

View File

@ -8,6 +8,7 @@ fragment AlertDetailItem on AlertManagementAlert {
service
description
updatedAt
endedAt
details
notes {
nodes {

View File

@ -4,7 +4,6 @@ fragment AlertListItem on AlertManagementAlert {
severity
status
startedAt
endedAt
eventCount
issueIid
assignees {

View File

@ -65,18 +65,10 @@ const getSeriesLabel = (queryLabel, metricAttributes) => {
*/
// eslint-disable-next-line import/prefer-default-export
export const makeDataSeries = (queryResults, defaultConfig) =>
queryResults
.map(result => {
// NaN values may disrupt avg., max. & min. calculations in the legend, filter them out
const data = result.values.filter(([, value]) => !Number.isNaN(value));
if (!data.length) {
return null;
}
const series = { data };
queryResults.map(result => {
return {
...defaultConfig,
...series,
data: result.values,
name: getSeriesLabel(defaultConfig.name, result.metric),
};
})
.filter(series => series !== null);
});

View File

@ -1,4 +1,9 @@
.alert-management-list {
// Migrate this to gitlab-ui
.gl-w-12 {
width: $gl-spacing-scale-12;
}
// these styles need to be deleted once GlTable component looks in GitLab same as in @gitlab/ui
table {
color: $gray-700;

View File

@ -30,7 +30,7 @@ class Projects::BlobController < Projects::ApplicationController
before_action :set_last_commit_sha, only: [:edit, :update]
before_action only: :show do
push_frontend_feature_flag(:code_navigation, @project)
push_frontend_feature_flag(:code_navigation, @project, default_enabled: true)
push_frontend_feature_flag(:suggest_pipeline) if experiment_enabled?(:suggest_pipeline)
end

View File

@ -29,7 +29,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:deploy_from_footer, @project, default_enabled: true)
push_frontend_feature_flag(:single_mr_diff_view, @project, default_enabled: true)
push_frontend_feature_flag(:suggest_pipeline) if experiment_enabled?(:suggest_pipeline)
push_frontend_feature_flag(:code_navigation, @project)
push_frontend_feature_flag(:code_navigation, @project, default_enabled: true)
push_frontend_feature_flag(:widget_visibility_polling, @project, default_enabled: true)
push_frontend_feature_flag(:merge_ref_head_comments, @project)
push_frontend_feature_flag(:mr_commit_neighbor_nav, @project, default_enabled: true)

View File

@ -110,7 +110,9 @@ class IssuableFinder
def group
strong_memoize(:group) do
if params[:group_id].present?
if params[:group_id].is_a?(Group)
params[:group_id]
elsif params[:group_id].present?
Group.find(params[:group_id])
else
nil

View File

@ -27,18 +27,13 @@ class IssuesFinder
end
def user_can_see_all_confidential_issues?
return @user_can_see_all_confidential_issues if defined?(@user_can_see_all_confidential_issues)
return @user_can_see_all_confidential_issues = false if current_user.blank?
return @user_can_see_all_confidential_issues = true if current_user.can_read_all_resources?
@user_can_see_all_confidential_issues =
if project? && project
project.team.max_member_access(current_user.id) >= CONFIDENTIAL_ACCESS_LEVEL
elsif group
group.max_member_access_for_user(current_user) >= CONFIDENTIAL_ACCESS_LEVEL
strong_memoize(:user_can_see_all_confidential_issues) do
parent = project? ? project : group
if parent
Ability.allowed?(current_user, :read_confidential_issues, parent)
else
false
Ability.allowed?(current_user, :read_all_resources)
end
end
end

View File

@ -9,30 +9,31 @@ module Mutations
end
def resolve_issuable(type:, parent_path:, iid:)
parent = resolve_issuable_parent(type, parent_path)
key = type == :merge_request ? :iids : :iid
args = { key => iid.to_s }
parent = ::Gitlab::Graphql::Lazy.force(resolve_issuable_parent(type, parent_path))
return unless parent.present?
resolver = issuable_resolver(type, parent, context)
ready, early_return = resolver.ready?(**args)
return early_return unless ready
resolver.resolve(**args)
finder = issuable_finder(type, iids: [iid])
Gitlab::Graphql::Loaders::IssuableLoader.new(parent, finder).find_all.first
end
private
def issuable_resolver(type, parent, context)
resolver_class = "Resolvers::#{type.to_s.classify.pluralize}Resolver".constantize
resolver_class.single.new(object: parent, context: context, field: nil)
def issuable_finder(type, args)
case type
when :merge_request
MergeRequestsFinder.new(current_user, args)
when :issue
IssuesFinder.new(current_user, args)
else
raise "Unsupported type: #{type}"
end
end
def resolve_issuable_parent(type, parent_path)
return unless parent_path.present?
return unless type == :issue || type == :merge_request
resolve_project(full_path: parent_path) if parent_path.present?
resolve_project(full_path: parent_path)
end
end
end

View File

@ -83,5 +83,10 @@ module Resolvers
def current_user
context[:current_user]
end
# Overridden in sub-classes (see .single, .last)
def select_result(results)
results
end
end
end

View File

@ -11,16 +11,10 @@ module ResolvesMergeRequests
end
def resolve_with_lookahead(**args)
args[:iids] = Array.wrap(args[:iids]) if args[:iids]
args.compact!
mr_finder = MergeRequestsFinder.new(current_user, args.compact)
finder = Gitlab::Graphql::Loaders::IssuableLoader.new(project, mr_finder)
if project && args.keys == [:iids]
batch_load_merge_requests(args[:iids])
else
args[:project_id] ||= project
apply_lookahead(MergeRequestsFinder.new(current_user, args).execute)
end.then(&(single? ? :first : :itself))
select_result(finder.batching_find_all { |query| apply_lookahead(query) })
end
def ready?(**args)
@ -35,22 +29,6 @@ module ResolvesMergeRequests
private
def batch_load_merge_requests(iids)
iids.map { |iid| batch_load(iid) }.select(&:itself) # .compact doesn't work on BatchLoader
end
# rubocop: disable CodeReuse/ActiveRecord
def batch_load(iid)
BatchLoader::GraphQL.for(iid.to_s).batch(key: project) do |iids, loader, args|
query = args[:key].merge_requests.where(iid: iids)
apply_lookahead(query).each do |mr|
loader.call(mr.iid.to_s, mr)
end
end
end
# rubocop: enable CodeReuse/ActiveRecord
def unconditional_includes
[:target_project]
end

View File

@ -63,18 +63,13 @@ module Resolvers
parent = object.respond_to?(:sync) ? object.sync : object
return Issue.none if parent.nil?
if parent.is_a?(Group)
args[:group_id] = parent.id
else
args[:project_id] = parent.id
end
# Will need to be be made group & namespace aware with
# https://gitlab.com/gitlab-org/gitlab-foss/issues/54520
args[:iids] ||= [args[:iid]].compact
args[:attempt_project_search_optimizations] = args[:search].present?
args[:iids] ||= [args.delete(:iid)].compact if args[:iid]
args[:attempt_project_search_optimizations] = true if args[:search].present?
issues = IssuesFinder.new(context[:current_user], args).execute
finder = IssuesFinder.new(current_user, args)
issues = Gitlab::Graphql::Loaders::IssuableLoader.new(parent, finder).batching_find_all
if non_stable_cursor_sort?(args[:sort])
# Certain complex sorts are not supported by the stable cursor pagination yet.

View File

@ -51,6 +51,8 @@ module Ci
has_many :latest_builds, -> { latest }, foreign_key: :commit_id, inverse_of: :pipeline, class_name: 'Ci::Build'
has_many :downloadable_artifacts, -> { not_expired.downloadable }, through: :latest_builds, source: :job_artifacts
has_many :messages, class_name: 'Ci::PipelineMessage', inverse_of: :pipeline
# Merge requests for which the current pipeline is running against
# the merge request's latest commit.
has_many :merge_requests_as_head_pipeline, foreign_key: "head_pipeline_id", class_name: 'MergeRequest'
@ -634,6 +636,12 @@ module Ci
yaml_errors.present?
end
def add_error_message(content)
return unless Gitlab::Ci::Features.store_pipeline_messages?(project)
messages.error.build(content: content)
end
# Manually set the notes for a Ci::Pipeline
# There is no ActiveRecord relation between Ci::Pipeline and notes
# as they are related to a commit sha. This method helps importing
@ -957,7 +965,7 @@ module Ci
stages.find_by!(name: name)
end
def error_messages
def full_error_messages
errors ? errors.full_messages.to_sentence : ""
end

View File

@ -0,0 +1,25 @@
# frozen_string_literal: true
module Ci
class PipelineMessage < ApplicationRecord
extend Gitlab::Ci::Model
MAX_CONTENT_LENGTH = 10_000
belongs_to :pipeline
validates :content, presence: true
before_save :truncate_long_content
enum severity: { error: 0, warning: 1 }
private
def truncate_long_content
return if content.length <= MAX_CONTENT_LENGTH
self.content = content.truncate(MAX_CONTENT_LENGTH)
end
end
end

View File

@ -167,6 +167,7 @@ class Project < ApplicationRecord
has_one :youtrack_service
has_one :custom_issue_tracker_service
has_one :bugzilla_service
has_one :gitlab_issue_tracker_service, inverse_of: :project
has_one :external_wiki_service
has_one :prometheus_service, inverse_of: :project
has_one :mock_ci_service

View File

@ -0,0 +1,45 @@
# frozen_string_literal: true
class GitlabIssueTrackerService < IssueTrackerService
include Gitlab::Routing
validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated?
default_value_for :default, true
def title
'GitLab'
end
def description
s_('IssueTracker|GitLab issue tracker')
end
def self.to_param
'gitlab'
end
def project_url
project_issues_url(project)
end
def new_issue_url
new_project_issue_url(project)
end
def issue_url(iid)
project_issue_url(project, id: iid)
end
def issue_tracker_path
project_issues_path(project)
end
def new_issue_path
new_project_issue_path(project)
end
def issue_path(iid)
project_issue_path(project, id: iid)
end
end

View File

@ -55,9 +55,11 @@ class Service < ApplicationRecord
validates :instance, uniqueness: { scope: :type }, if: -> { instance? }
validate :validate_is_instance_or_template
scope :visible, -> { where.not(type: 'GitlabIssueTrackerService') }
scope :issue_trackers, -> { where(category: 'issue_tracker') }
scope :external_wikis, -> { where(type: 'ExternalWikiService').active }
scope :active, -> { where(active: true) }
scope :without_defaults, -> { where(default: false) }
scope :by_type, -> (type) { where(type: type) }
scope :by_active_flag, -> (flag) { where(active: flag) }
scope :templates, -> { where(template: true, type: available_services_types) }
@ -75,7 +77,7 @@ class Service < ApplicationRecord
scope :wiki_page_hooks, -> { where(wiki_page_events: true, active: true) }
scope :deployment_hooks, -> { where(deployment_events: true, active: true) }
scope :alert_hooks, -> { where(alert_events: true, active: true) }
scope :external_issue_trackers, -> { issue_trackers.active }
scope :external_issue_trackers, -> { issue_trackers.active.without_defaults }
scope :deployment, -> { where(category: 'deployment') }
default_value_for :category, 'common'

View File

@ -58,6 +58,8 @@ class BasePolicy < DeclarativePolicy::Base
rule { admin }.enable :read_all_resources
rule { default }.enable :read_cross_project
condition(:is_gitlab_com) { ::Gitlab.dev_env_or_com? }
end
BasePolicy.prepend_if_ee('EE::BasePolicy')

View File

@ -67,6 +67,10 @@ class GroupPolicy < BasePolicy
enable :update_max_artifacts_size
end
rule { can?(:read_all_resources) }.policy do
enable :read_confidential_issues
end
rule { has_projects }.policy do
enable :read_group
end

View File

@ -176,6 +176,7 @@ class ProjectPolicy < BasePolicy
rule { guest | admin }.enable :read_project_for_iids
rule { admin }.enable :update_max_artifacts_size
rule { can?(:read_all_resources) }.enable :read_confidential_issues
rule { guest }.enable :guest_access
rule { reporter }.enable :reporter_access
@ -257,6 +258,7 @@ class ProjectPolicy < BasePolicy
enable :read_prometheus
enable :read_metrics_dashboard_annotation
enable :metrics_dashboard
enable :read_confidential_issues
end
# We define `:public_user_access` separately because there are cases in gitlab-ee

View File

@ -95,7 +95,7 @@ module Ci
def execute!(*args, &block)
execute(*args, &block).tap do |pipeline|
unless pipeline.persisted?
raise CreateError, pipeline.error_messages
raise CreateError, pipeline.full_error_messages
end
end
end

View File

@ -57,7 +57,7 @@
= f.text_field :user_default_internal_regex, placeholder: _('Regex pattern'), class: 'form-control prepend-top-5'
.help-block
= _('Specify an e-mail address regex pattern to identify default internal users.')
= link_to _('More information'), help_page_path('user/permissions', anchor: 'external-users-permissions'),
= link_to _('More information'), help_page_path('user/permissions', anchor: 'setting-new-users-to-external'),
target: '_blank'
.form-group
= f.label :user_show_add_ssh_key_message, _('Prompt users to upload SSH keys'), class: 'label-bold'

View File

@ -40,7 +40,7 @@
= f.text_field :default_artifacts_expire_in, class: 'form-control'
.form-text.text-muted
= _("Set the default expiration time for each job's artifacts. 0 for unlimited. The default unit is in seconds, but you can define an alternative. For example: <code>4 mins 2 sec</code>, <code>2h42min</code>.").html_safe
= link_to icon('question-circle'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'default-artifacts-expiration')
= link_to icon('question-circle'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'default-artifacts-expiration-core-only')
.form-group
= f.label :archive_builds_in_human_readable, _('Archive jobs'), class: 'label-bold'
= f.text_field :archive_builds_in_human_readable, class: 'form-control', placeholder: 'never'

View File

@ -2,7 +2,7 @@
%h4
= _('Variables')
= link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'variables'), target: '_blank', rel: 'noopener noreferrer'
= link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'custom-environment-variables'), target: '_blank', rel: 'noopener noreferrer'
%button.btn.btn-default.js-settings-toggle{ type: 'button' }
= expanded ? _('Collapse') : _('Expand')

View File

@ -1,3 +1,3 @@
= _('Environment variables are applied to environments via the runner. They can be protected by only exposing them to protected branches or tags. Additionally, they can be masked so they are hidden in job logs, though they must match certain regexp requirements to do so. You can use environment variables for passwords, secret keys, or whatever you want.')
= _('You may also add variables that are made available to the running application by prepending the variable key with <code>K8S_SECRET_</code>.').html_safe
= link_to _('More information'), help_page_path('ci/variables/README', anchor: 'variables')
= link_to _('More information'), help_page_path('ci/variables/README', anchor: 'custom-environment-variables')

View File

@ -2,7 +2,7 @@
%h4
= _('Variables')
= link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'variables'), target: '_blank', rel: 'noopener noreferrer'
= link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'custom-environment-variables'), target: '_blank', rel: 'noopener noreferrer'
%button.btn.btn-default.js-settings-toggle{ type: 'button' }
= expanded ? _('Collapse') : _('Expand')

View File

@ -39,7 +39,7 @@
= value
%p.masking-validation-error.gl-field-error.hide
= s_("CiVariables|Cannot use Masked Variable with current value")
= link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'masked-variables'), target: '_blank', rel: 'noopener noreferrer'
= link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'mask-a-custom-variable'), target: '_blank', rel: 'noopener noreferrer'
- unless only_key_value
.ci-variable-body-item.ci-variable-protected-item.table-section.section-20.mr-0.border-top-0
.append-right-default

View File

@ -70,7 +70,7 @@
label_class: 'label-bold' }
.form-text.text-muted
= s_('ClusterIntegration|Uses the Cloud Run, Istio, and HTTP Load Balancing addons for this cluster.')
= link_to _('More information'), help_page_path('user/project/clusters/add_remove_clusters.md', anchor: 'cloud-run-for-anthos'), target: '_blank'
= link_to _('More information'), help_page_path('user/project/clusters/add_gke_clusters.md', anchor: 'cloud-run-for-anthos'), target: '_blank'
.form-group
= field.check_box :managed, { label: s_('ClusterIntegration|GitLab-managed cluster'),

View File

@ -33,5 +33,5 @@
.flash-notice
.flash-text
= s_("PrometheusService|To set up automatic monitoring, add the environment variable %{variable} to exporter's queries." % { variable: "<code>$CI_ENVIRONMENT_SLUG</code>" }).html_safe
= link_to s_('PrometheusService|More information'), help_page_path('user/project/integrations/prometheus', anchor: 'metrics-and-labels')
= link_to s_('PrometheusService|More information'), help_page_path('user/project/integrations/prometheus', anchor: 'query-variables')
%ul.list-unstyled.metrics-list.js-missing-var-metrics-list

View File

@ -40,18 +40,18 @@
= form.radio_button :deploy_strategy, 'continuous', class: 'form-check-input'
= form.label :deploy_strategy_continuous, class: 'form-check-label' do
= s_('CICD|Continuous deployment to production')
= link_to icon('question-circle'), help_page_path('topics/autodevops/index.md', anchor: 'auto-deploy'), target: '_blank'
= link_to icon('question-circle'), help_page_path('topics/autodevops/stages.md', anchor: 'auto-deploy'), target: '_blank'
.form-check
= form.radio_button :deploy_strategy, 'timed_incremental', class: 'form-check-input'
= form.label :deploy_strategy_timed_incremental, class: 'form-check-label' do
= s_('CICD|Continuous deployment to production using timed incremental rollout')
= link_to icon('question-circle'), help_page_path('topics/autodevops/index.md', anchor: 'timed-incremental-rollout-to-production-premium'), target: '_blank'
= link_to icon('question-circle'), help_page_path('topics/autodevops/customize.md', anchor: 'timed-incremental-rollout-to-production-premium'), target: '_blank'
.form-check
= form.radio_button :deploy_strategy, 'manual', class: 'form-check-input'
= form.label :deploy_strategy_manual, class: 'form-check-label' do
= s_('CICD|Automatic deployment to staging, manual deployment to production')
= link_to icon('question-circle'), help_page_path('topics/autodevops/index.md', anchor: 'incremental-rollout-to-production-premium'), target: '_blank'
= link_to icon('question-circle'), help_page_path('topics/autodevops/customize.md', anchor: 'incremental-rollout-to-production-premium'), target: '_blank'
= f.submit _('Save changes'), class: "btn btn-success prepend-top-15", data: { qa_selector: 'save_changes_button' }

View File

@ -0,0 +1,5 @@
---
title: Stop removing NaN values from monitoring data series
merge_request: 35086
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Update UI links to docs in core features
merge_request: 35488
author:
type: other

View File

@ -0,0 +1,5 @@
---
title: Expose snippets_size in ProjectStatistics Entity
merge_request: 35316
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Store pipeline creation errors and warnings into Ci::PipelineMessage
merge_request: 33762
author:
type: other

View File

@ -0,0 +1,5 @@
---
title: Improve alert list spacing
merge_request: 35400
author:
type: fixed

View File

@ -0,0 +1,27 @@
# frozen_string_literal: true
class CreateCiPipelineMessagesTable < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
ERROR_SEVERITY = 0
MAX_CONTENT_LENGTH = 10_000
disable_ddl_transaction!
def up
with_lock_retries do
create_table :ci_pipeline_messages do |t|
t.integer :severity, null: false, default: ERROR_SEVERITY, limit: 2
t.references :pipeline, index: true, null: false, foreign_key: { to_table: :ci_pipelines, on_delete: :cascade }, type: :integer
t.text :content, null: false
end
end
add_text_limit :ci_pipeline_messages, :content, MAX_CONTENT_LENGTH
end
def down
drop_table :ci_pipeline_messages
end
end

View File

@ -9936,6 +9936,23 @@ CREATE SEQUENCE public.ci_pipeline_chat_data_id_seq
ALTER SEQUENCE public.ci_pipeline_chat_data_id_seq OWNED BY public.ci_pipeline_chat_data.id;
CREATE TABLE public.ci_pipeline_messages (
id bigint NOT NULL,
severity smallint DEFAULT 0 NOT NULL,
pipeline_id integer NOT NULL,
content text NOT NULL,
CONSTRAINT check_58ca2981b2 CHECK ((char_length(content) <= 10000))
);
CREATE SEQUENCE public.ci_pipeline_messages_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE public.ci_pipeline_messages_id_seq OWNED BY public.ci_pipeline_messages.id;
CREATE TABLE public.ci_pipeline_schedule_variables (
id integer NOT NULL,
key character varying NOT NULL,
@ -16293,6 +16310,8 @@ ALTER TABLE ONLY public.ci_job_variables ALTER COLUMN id SET DEFAULT nextval('pu
ALTER TABLE ONLY public.ci_pipeline_chat_data ALTER COLUMN id SET DEFAULT nextval('public.ci_pipeline_chat_data_id_seq'::regclass);
ALTER TABLE ONLY public.ci_pipeline_messages ALTER COLUMN id SET DEFAULT nextval('public.ci_pipeline_messages_id_seq'::regclass);
ALTER TABLE ONLY public.ci_pipeline_schedule_variables ALTER COLUMN id SET DEFAULT nextval('public.ci_pipeline_schedule_variables_id_seq'::regclass);
ALTER TABLE ONLY public.ci_pipeline_schedules ALTER COLUMN id SET DEFAULT nextval('public.ci_pipeline_schedules_id_seq'::regclass);
@ -17203,6 +17222,9 @@ ALTER TABLE ONLY public.ci_job_variables
ALTER TABLE ONLY public.ci_pipeline_chat_data
ADD CONSTRAINT ci_pipeline_chat_data_pkey PRIMARY KEY (id);
ALTER TABLE ONLY public.ci_pipeline_messages
ADD CONSTRAINT ci_pipeline_messages_pkey PRIMARY KEY (id);
ALTER TABLE ONLY public.ci_pipeline_schedule_variables
ADD CONSTRAINT ci_pipeline_schedule_variables_pkey PRIMARY KEY (id);
@ -18605,6 +18627,8 @@ CREATE INDEX index_ci_pipeline_chat_data_on_chat_name_id ON public.ci_pipeline_c
CREATE UNIQUE INDEX index_ci_pipeline_chat_data_on_pipeline_id ON public.ci_pipeline_chat_data USING btree (pipeline_id);
CREATE INDEX index_ci_pipeline_messages_on_pipeline_id ON public.ci_pipeline_messages USING btree (pipeline_id);
CREATE UNIQUE INDEX index_ci_pipeline_schedule_variables_on_schedule_id_and_key ON public.ci_pipeline_schedule_variables USING btree (pipeline_schedule_id, key);
CREATE INDEX index_ci_pipeline_schedules_on_next_run_at_and_active ON public.ci_pipeline_schedules USING btree (next_run_at, active);
@ -21844,6 +21868,9 @@ ALTER TABLE ONLY public.packages_conan_metadata
ALTER TABLE ONLY public.vulnerability_feedback
ADD CONSTRAINT fk_rails_8c77e5891a FOREIGN KEY (issue_id) REFERENCES public.issues(id) ON DELETE SET NULL;
ALTER TABLE ONLY public.ci_pipeline_messages
ADD CONSTRAINT fk_rails_8d3b04e3e1 FOREIGN KEY (pipeline_id) REFERENCES public.ci_pipelines(id) ON DELETE CASCADE;
ALTER TABLE ONLY public.approval_merge_request_rules_approved_approvers
ADD CONSTRAINT fk_rails_8dc94cff4d FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE;
@ -23417,6 +23444,7 @@ COPY "schema_migrations" (version) FROM STDIN;
20200622070620
20200622095419
20200622103836
20200622104923
20200622235737
20200623000148
20200623000320

View File

@ -175,7 +175,8 @@ When the user is authenticated and `simple` is not set this returns something li
"wiki_size" : 0,
"lfs_objects_size": 0,
"job_artifacts_size": 0,
"packages_size": 0
"packages_size": 0,
"snippets_size": 0
},
"_links": {
"self": "http://example.com/api/v4/projects",
@ -277,7 +278,8 @@ When the user is authenticated and `simple` is not set this returns something li
"wiki_size" : 0,
"lfs_objects_size": 0,
"job_artifacts_size": 0,
"packages_size": 0
"packages_size": 0,
"snippets_size": 0
},
"_links": {
"self": "http://example.com/api/v4/projects",
@ -426,7 +428,8 @@ This endpoint supports [keyset pagination](README.md#keyset-based-pagination) fo
"wiki_size" : 0,
"lfs_objects_size": 0,
"job_artifacts_size": 0,
"packages_size": 0
"packages_size": 0,
"snippets_size": 0
},
"_links": {
"self": "http://example.com/api/v4/projects",
@ -528,7 +531,8 @@ This endpoint supports [keyset pagination](README.md#keyset-based-pagination) fo
"wiki_size" : 0,
"lfs_objects_size": 0,
"job_artifacts_size": 0,
"packages_size": 0
"packages_size": 0,
"snippets_size": 0
},
"_links": {
"self": "http://example.com/api/v4/projects",
@ -899,7 +903,8 @@ GET /projects/:id
"wiki_size" : 0,
"lfs_objects_size": 0,
"job_artifacts_size": 0,
"packages_size": 0
"packages_size": 0,
"snippets_size": 0
},
"_links": {
"self": "http://example.com/api/v4/projects",

View File

@ -118,21 +118,17 @@ The sort option and order is saved and used wherever you browse epics, including
## Make an epic confidential
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/213068) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.0.
> - It's deployed behind a feature flag, disabled by default.
> - It's disabled on GitLab.com.
> - It's not recommended for production use.
> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-confidential-epics-premium-only). **(PREMIUM ONLY)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/213068) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.0 behind a feature flag, disabled by default.
When you're creating an epic, you can make it confidential by selecting the **Make this epic
confidential** checkbox.
### Enable Confidential Epics **(PREMIUM ONLY)**
### Enable confidential epics **(PREMIUM ONLY)**
The Confidential Epics feature is under development and not ready for production use.
It's deployed behind a feature flag that is **disabled by default**.
The confidential epics feature is under development and not ready for production use. It's 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.
can enable it for your self-managed instance.
To enable it:

View File

@ -289,8 +289,43 @@ Once the Code Quality job has completed:
[downloadable artifact](../../../ci/pipelines/job_artifacts.md#downloading-artifacts)
for the `code_quality` job.
## Extending functionality
### Using Analysis Plugins
Should there be a need to extend the default functionality provided by Code Quality, as stated in [Code Quality](#code-quality-starter), [Analysis Plugins](https://docs.codeclimate.com/docs/list-of-engines) are available.
For example, to use the [SonarJava analyzer](https://docs.codeclimate.com/docs/sonar-java),
add a file named `.codeclimate.yml` containing the [enablement code](https://docs.codeclimate.com/docs/sonar-java#enable-the-plugin)
for the plugin to the root of your repository:
```yaml
version: "2"
plugins:
sonar-java:
enabled: true
```
This adds SonarJava to the `plugins:` section of the [default `.codeclimate.yml`](https://gitlab.com/gitlab-org/ci-cd/codequality/-/blob/master/codeclimate_defaults/.codeclimate.yml)
included in your project.
Changes to the `plugins:` section do not affect the `exclude_patterns` section of the
defeault `.codeclimate.yml`. See the Code Climate documentation for
[excluding files and folders](https://docs.codeclimate.com/docs/excluding-files-and-folders)
for more details.
Here's [an example project](https://gitlab.com/jheimbuck_gl/jh_java_example_project) that uses Code Quality with a `.codeclimate.yml` file.
## Troubleshooting
### Changing the default configuration has no effect
A common issue is that the terms `Code Quality` (GitLab specific) and `Code Climate`
(Engine used by GitLab) are very similar. You must add a **`.codeclimate.yml`** file
to change the default configuration, **not** a `.codequality.yml` file. If you use
the wrong filename, the [default `.codeclimate.yml`](https://gitlab.com/gitlab-org/ci-cd/codequality/-/blob/master/codeclimate_defaults/.codeclimate.yml)
is still used.
### No Code Quality report is displayed in a Merge Request
This can be due to multiple reasons:

View File

@ -9,6 +9,7 @@ module API
expose :wiki_size
expose :lfs_objects_size
expose :build_artifacts_size, as: :job_artifacts_size
expose :snippets_size
end
end
end

View File

@ -23,6 +23,10 @@ module Banzai
issue_url(issue, project)
end
def projects_relation_for_paths(paths)
super(paths).includes(:gitlab_issue_tracker_service)
end
def parent_records(parent, ids)
parent.issues.where(iid: ids.to_a)
end

View File

@ -45,6 +45,11 @@ module Gitlab
def self.release_generation_enabled?
::Feature.enabled?(:ci_release_generation)
end
# Remove in https://gitlab.com/gitlab-org/gitlab/-/issues/224199
def self.store_pipeline_messages?(project)
::Feature.enabled?(:ci_store_pipeline_messages, project, default_enabled: true)
end
end
end
end

View File

@ -11,7 +11,12 @@ module Gitlab
pipeline.yaml_errors = message
end
pipeline.add_error_message(message)
pipeline.drop!(drop_reason) if drop_reason
# TODO: consider not to rely on AR errors directly as they can be
# polluted with other unrelated errors (e.g. state machine)
# https://gitlab.com/gitlab-org/gitlab/-/issues/220823
pipeline.errors.add(:base, message)
end
end

View File

@ -30,8 +30,7 @@ module Gitlab
end
def authorized_find!(*args)
object = find_object(*args)
object = object.sync if object.respond_to?(:sync)
object = Graphql::Lazy.force(find_object(*args))
authorize!(object)

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
module Gitlab
module Graphql
class Lazy
# Force evaluation of a (possibly) lazy value
def self.force(value)
case value
when ::BatchLoader::GraphQL
value.sync
when ::Concurrent::Promise
value.execute.value
else
value
end
end
end
end
end

View File

@ -0,0 +1,82 @@
# frozen_string_literal: true
module Gitlab
module Graphql
module Loaders
class IssuableLoader
attr_reader :parent, :issuable_finder
BatchKey = Struct.new(:parent, :finder_class, :current_user)
def initialize(parent, issuable_finder)
@parent = parent
@issuable_finder = issuable_finder
end
def batching_find_all(&with_query)
if issuable_finder.params.keys == ['iids']
batch_load_issuables(issuable_finder.params[:iids], with_query)
else
post_process(find_all, with_query)
end
end
def find_all
issuable_finder.params[parent_param] = parent if parent
issuable_finder.execute
end
private
def parent_param
case parent
when Project
:project_id
when Group
:group_id
else
raise "Unexpected parent: #{parent.class}"
end
end
def post_process(query, with_query)
if with_query
with_query.call(query)
else
query
end
end
def batch_load_issuables(iids, with_query)
Array.wrap(iids).map { |iid| batch_load(iid, with_query) }
end
def batch_load(iid, with_query)
return if parent.nil?
BatchLoader::GraphQL
.for([parent_param, iid.to_s])
.batch(key: batch_key) do |params, loader, args|
batch_key = args[:key]
user = batch_key.current_user
params.group_by(&:first).each do |key, group|
iids = group.map(&:second).uniq
args = { key => batch_key.parent, iids: iids }
query = batch_key.finder_class.new(user, args).execute
post_process(query, with_query).each do |item|
loader.call([key, item.iid.to_s], item)
end
end
end
end
def batch_key
BatchKey.new(parent, issuable_finder.class, issuable_finder.current_user)
end
end
end
end
end

View File

@ -52,8 +52,7 @@ module Gitlab
end
def duration(time_started)
nanoseconds = Gitlab::Metrics::System.monotonic_time - time_started
nanoseconds * 1000000
Gitlab::Metrics::System.monotonic_time - time_started
end
def default_initial_values(query)

View File

@ -1951,9 +1951,6 @@ msgstr ""
msgid "AlertManagement|Edit"
msgstr ""
msgid "AlertManagement|End time"
msgstr ""
msgid "AlertManagement|Events"
msgstr ""
@ -10295,6 +10292,9 @@ msgstr ""
msgid "Geo allows you to replicate your GitLab instance to other geographical locations."
msgstr ""
msgid "Geo nodes are paused using a command run on the node"
msgstr ""
msgid "GeoNodeStatusEvent|%{timeAgoStr} (%{pendingEvents} events)"
msgstr ""
@ -10400,6 +10400,9 @@ msgstr ""
msgid "GeoNodes|Replication slots"
msgstr ""
msgid "GeoNodes|Replication status"
msgstr ""
msgid "GeoNodes|Repositories"
msgstr ""
@ -12593,6 +12596,9 @@ msgstr ""
msgid "IssueTracker|Custom issue tracker"
msgstr ""
msgid "IssueTracker|GitLab issue tracker"
msgstr ""
msgid "IssueTracker|Redmine issue tracker"
msgstr ""
@ -19049,6 +19055,12 @@ msgstr ""
msgid "Replication"
msgstr ""
msgid "Replication enabled"
msgstr ""
msgid "Replication paused"
msgstr ""
msgid "Reply by email"
msgstr ""
@ -19424,9 +19436,6 @@ msgstr ""
msgid "Resume"
msgstr ""
msgid "Resume replication"
msgstr ""
msgid "Resync"
msgstr ""

View File

@ -107,6 +107,12 @@ FactoryBot.define do
issue_tracker
end
factory :gitlab_issue_tracker_service do
project
active { true }
issue_tracker
end
trait :issue_tracker do
transient do
create_data { true }

View File

@ -20,7 +20,7 @@ RSpec.describe 'a maintainer edits files on a source-branch of an MR from a fork
end
before do
stub_feature_flags(web_ide_default: false, single_mr_diff_view: false, code_navigation: false)
stub_feature_flags(web_ide_default: false, single_mr_diff_view: false)
target_project.add_maintainer(user)
sign_in(user)

View File

@ -13,10 +13,6 @@ RSpec.describe 'File blob', :js do
wait_for_requests
end
before do
stub_feature_flags(code_navigation: false)
end
context 'Ruby file' do
before do
visit_blob('files/ruby/popen.rb')

View File

@ -77,8 +77,6 @@ RSpec.describe 'Editing file blob', :js do
context 'from blob file path' do
before do
stub_feature_flags(code_navigation: false)
visit project_blob_path(project, tree_join(branch, file_path))
end

View File

@ -8,7 +8,6 @@ RSpec.describe 'User creates blob in new project', :js do
shared_examples 'creating a file' do
before do
stub_feature_flags(code_navigation: false)
sign_in(user)
visit project_path(project)
end

View File

@ -14,7 +14,7 @@ RSpec.describe 'Projects > Files > User creates files', :js do
let(:user) { create(:user) }
before do
stub_feature_flags(web_ide_default: false, code_navigation: false)
stub_feature_flags(web_ide_default: false)
project.add_maintainer(user)
sign_in(user)

View File

@ -14,8 +14,6 @@ RSpec.describe 'Projects > Files > User deletes files', :js do
let(:user) { create(:user) }
before do
stub_feature_flags(code_navigation: false)
sign_in(user)
end

View File

@ -16,8 +16,6 @@ RSpec.describe 'Projects > Files > User replaces files', :js do
let(:user) { create(:user) }
before do
stub_feature_flags(code_navigation: false)
sign_in(user)
end

View File

@ -289,7 +289,6 @@ describe('AlertManagementList', () => {
iid: 1,
status: 'acknowledged',
startedAt: '2020-03-17T23:18:14.996Z',
endedAt: '2020-04-17T23:18:14.996Z',
severity: 'high',
assignees: { nodes: [] },
},
@ -300,7 +299,7 @@ describe('AlertManagementList', () => {
},
loading: false,
});
expect(findDateFields().length).toBe(2);
expect(findDateFields().length).toBe(1);
});
it('should not display time ago dates when values not provided', () => {
@ -312,7 +311,6 @@ describe('AlertManagementList', () => {
iid: 1,
status: 'acknowledged',
startedAt: null,
endedAt: null,
severity: 'high',
},
],

View File

@ -33,15 +33,6 @@ describe('monitor helper', () => {
]);
});
it('excludes NaN values', () => {
expect(
monitorHelper.makeDataSeries(
data({ metric: {}, values: [[1, 1], [2, NaN]] }),
defaultConfig,
),
).toEqual([{ ...expectedDataSeries[0], data: [[1, 1]] }]);
});
it('updates series name from templates', () => {
const config = {
...defaultConfig,

View File

@ -815,6 +815,50 @@ describe('normalizeQueryResponseData', () => {
},
]);
});
it('processes a scalar result with a NaN result', () => {
// Queries may return "NaN" string values.
// e.g. when Prometheus cannot find a metric the query
// `scalar(does_not_exist)` will return a "NaN" value.
const mockScalar = {
resultType: 'scalar',
result: [1435781451.781, 'NaN'],
};
expect(normalizeQueryResponseData(mockScalar)).toEqual([
{
metric: {},
value: ['2015-07-01T20:10:51.781Z', NaN],
values: [['2015-07-01T20:10:51.781Z', NaN]],
},
]);
});
it('processes a matrix result with a "NaN" value', () => {
// Queries may return "NaN" string values.
const mockMatrix = {
resultType: 'matrix',
result: [
{
metric: {
__name__: 'up',
job: 'prometheus',
instance: 'localhost:9090',
},
values: [[1435781430.781, '1'], [1435781460.781, 'NaN']],
},
],
};
expect(normalizeQueryResponseData(mockMatrix)).toEqual([
{
metric: { __name__: 'up', instance: 'localhost:9090', job: 'prometheus' },
value: ['2015-07-01T20:11:00.781Z', NaN],
values: [['2015-07-01T20:10:30.781Z', 1], ['2015-07-01T20:11:00.781Z', NaN]],
},
]);
});
});
describe('normalizeCustomDashboardPath', () => {

View File

@ -3,26 +3,31 @@
require 'spec_helper'
RSpec.describe Mutations::ResolvesIssuable do
include GraphqlHelpers
let_it_be(:mutation_class) do
Class.new(Mutations::BaseMutation) do
include Mutations::ResolvesIssuable
end
end
let_it_be(:project) { create(:project) }
let_it_be(:project) { create(:project, :empty_repo) }
let_it_be(:user) { create(:user) }
let_it_be(:context) { { current_user: user } }
let_it_be(:mutation) { mutation_class.new(object: nil, context: context, field: nil) }
let(:mutation) { mutation_class.new(object: nil, context: context, field: nil) }
let(:parent) { issuable.project }
let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:merge_request) { create(:merge_request, source_project: project) }
context 'with issues' do
let(:issuable) { create(:issue, project: project) }
let(:issuable) { issue }
it_behaves_like 'resolving an issuable in GraphQL', :issue
end
context 'with merge requests' do
let(:issuable) { create(:merge_request, source_project: project) }
let(:issuable) { merge_request }
it_behaves_like 'resolving an issuable in GraphQL', :merge_request
end

View File

@ -101,12 +101,10 @@ RSpec.describe Resolvers::IssuesResolver do
end
it 'uses project search optimization' do
expected_arguments = {
expected_arguments = a_hash_including(
search: 'foo',
attempt_project_search_optimizations: true,
iids: [],
project_id: project.id
}
attempt_project_search_optimizations: true
)
expect(IssuesFinder).to receive(:new).with(anything, expected_arguments).and_call_original
resolve_issues(search: 'foo')
@ -217,16 +215,32 @@ RSpec.describe Resolvers::IssuesResolver do
expect(resolve_issues).to contain_exactly(issue1, issue2)
end
it 'finds a specific issue with iid' do
expect(resolve_issues(iid: issue1.iid)).to contain_exactly(issue1)
it 'finds a specific issue with iid', :request_store do
result = batch_sync(max_queries: 2) { resolve_issues(iid: issue1.iid) }
expect(result).to contain_exactly(issue1)
end
it 'finds a specific issue with iids' do
expect(resolve_issues(iids: issue1.iid)).to contain_exactly(issue1)
it 'batches queries that only include IIDs', :request_store do
result = batch_sync(max_queries: 2) do
resolve_issues(iid: issue1.iid) + resolve_issues(iids: issue2.iid)
end
expect(result).to contain_exactly(issue1, issue2)
end
it 'finds a specific issue with iids', :request_store do
result = batch_sync(max_queries: 2) do
resolve_issues(iids: [issue1.iid])
end
expect(result).to contain_exactly(issue1)
end
it 'finds multiple issues with iids' do
expect(resolve_issues(iids: [issue1.iid, issue2.iid]))
create(:issue, project: project, author: current_user)
expect(batch_sync { resolve_issues(iids: [issue1.iid, issue2.iid]) })
.to contain_exactly(issue1, issue2)
end
@ -238,7 +252,7 @@ RSpec.describe Resolvers::IssuesResolver do
create(:issue, project: another_project, iid: iid)
end
expect(resolve_issues(iids: iids)).to contain_exactly(issue1, issue2)
expect(batch_sync { resolve_issues(iids: iids) }).to contain_exactly(issue1, issue2)
end
end
end

View File

@ -22,9 +22,12 @@ RSpec.describe Resolvers::MergeRequestsResolver do
before do
project.add_developer(current_user)
other_project.add_developer(current_user)
end
describe '#resolve' do
let(:queries_per_project) { 3 }
context 'no arguments' do
it 'returns all merge requests' do
result = resolve_mr(project, {})
@ -40,24 +43,34 @@ RSpec.describe Resolvers::MergeRequestsResolver do
end
context 'by iid alone' do
it 'batch-resolves by target project full path and individual IID' do
result = batch_sync(max_queries: 2) do
it 'batch-resolves by target project full path and individual IID', :request_store do
# 1 query for project_authorizations, and 1 for merge_requests
result = batch_sync(max_queries: queries_per_project) do
[iid_1, iid_2].map { |iid| resolve_mr_single(project, iid) }
end
expect(result).to contain_exactly(merge_request_1, merge_request_2)
end
it 'batch-resolves by target project full path and IIDS' do
result = batch_sync(max_queries: 2) do
it 'batch-resolves by target project full path and IIDS', :request_store do
result = batch_sync(max_queries: queries_per_project) do
resolve_mr(project, iids: [iid_1, iid_2])
end
expect(result).to contain_exactly(merge_request_1, merge_request_2)
end
it 'batch-resolves by target project full path and IIDS, single or plural', :request_store do
result = batch_sync(max_queries: queries_per_project) do
[resolve_mr_single(project, merge_request_3.iid), *resolve_mr(project, iids: [iid_1, iid_2])]
end
expect(result).to contain_exactly(merge_request_1, merge_request_2, merge_request_3)
end
it 'can batch-resolve merge requests from different projects' do
result = batch_sync(max_queries: 3) do
# 2 queries for project_authorizations, and 2 for merge_requests
result = batch_sync(max_queries: queries_per_project * 2) do
resolve_mr(project, iids: iid_1) +
resolve_mr(project, iids: iid_2) +
resolve_mr(other_project, iids: other_iid)

View File

@ -0,0 +1,96 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Graphql::Loaders::IssuableLoader do
subject { described_class.new(parent, finder) }
let(:params) { HashWithIndifferentAccess.new }
describe '#find_all' do
let(:finder) { double(:finder, params: params, execute: %i[x y z]) }
where(:factory, :param_name) do
%i[project group].map { |thing| [thing, :"#{thing}_id"] }
end
with_them do
let(:parent) { build_stubbed(factory) }
it 'assignes the parent parameter, and batching_find_alls the finder' do
expect(subject.find_all).to contain_exactly(:x, :y, :z)
expect(params).to include(param_name => parent)
end
end
context 'the parent is of an unexpected type' do
let(:parent) { build(:merge_request) }
it 'raises an error if we pass an unexpected parent' do
expect { subject.find_all }.to raise_error(/Unexpected parent/)
end
end
end
describe '#batching_find_all' do
context 'the finder params are anything other than [iids]' do
let(:finder) { double(:finder, params: params, execute: [:foo]) }
let(:parent) { build_stubbed(:project) }
it 'batching_find_alls the finder, setting the correct parent parameter' do
expect(subject.batching_find_all).to eq([:foo])
expect(params[:project_id]).to eq(parent)
end
it 'allows a post-process block' do
expect(subject.batching_find_all(&:first)).to eq(:foo)
end
end
context 'the finder params are exactly [iids]' do
# Dumb finder class, that only implements what we need, and has
# predictable query counts.
let(:finder_class) do
Class.new do
attr_reader :current_user, :params
def initialize(user, args)
@current_user = user
@params = HashWithIndifferentAccess.new(args.to_h)
end
def execute
params[:project_id].issues.where(iid: params[:iids])
end
end
end
it 'batches requests' do
issue_a = create(:issue)
issue_b = create(:issue)
issue_c = create(:issue, project: issue_a.project)
proj_1 = issue_a.project
proj_2 = issue_b.project
user = create(:user, developer_projects: [proj_1, proj_2])
finder_a = finder_class.new(user, iids: [issue_a.iid])
finder_b = finder_class.new(user, iids: [issue_b.iid])
finder_c = finder_class.new(user, iids: [issue_c.iid])
results = []
expect do
results.concat(described_class.new(proj_1, finder_a).batching_find_all)
results.concat(described_class.new(proj_2, finder_b).batching_find_all)
results.concat(described_class.new(proj_1, finder_c).batching_find_all)
end.not_to exceed_query_limit(0)
expect do
results = results.map(&:sync)
end.not_to exceed_query_limit(2)
expect(results).to contain_exactly(issue_a, issue_b, issue_c)
end
end
end
end

View File

@ -17,9 +17,27 @@ RSpec.describe Gitlab::Graphql::QueryAnalyzers::LoggerAnalyzer do
end
context 'feature flag enabled by default' do
let(:monotonic_time_before) { 42 }
let(:monotonic_time_after) { 500 }
let(:monotonic_time_duration) { monotonic_time_after - monotonic_time_before }
it 'enables the analyzer' do
expect(subject.analyze?(anything)).to be_truthy
end
it 'returns a duration in seconds' do
allow(GraphQL::Analysis).to receive(:analyze_query).and_return([4, 2])
allow(Gitlab::Metrics::System).to receive(:monotonic_time).and_return(monotonic_time_before, monotonic_time_after)
allow(Gitlab::GraphqlLogger).to receive(:info)
expected_duration = monotonic_time_duration
memo = subject.initial_value(spy('query'))
subject.final_value(memo)
expect(memo).to have_key(:duration_s)
expect(memo[:duration_s]).to eq(expected_duration)
end
end
end
end

View File

@ -228,6 +228,7 @@ ci_pipelines:
- latest_builds
- daily_report_results
- latest_builds_report_results
- messages
ci_refs:
- project
- ci_pipelines
@ -349,6 +350,7 @@ project:
- youtrack_service
- custom_issue_tracker_service
- bugzilla_service
- gitlab_issue_tracker_service
- external_wiki_service
- mock_ci_service
- mock_deployment_service

View File

@ -0,0 +1,53 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Ci::PipelineMessage do
describe 'validations' do
subject { described_class.new(pipeline: pipeline, content: content) }
let(:pipeline) { create(:ci_pipeline) }
context 'when message content is longer than the limit' do
let(:content) { 'x' * (described_class::MAX_CONTENT_LENGTH + 1) }
it 'is truncated with ellipsis' do
subject.save!
expect(subject.content).to end_with('x...')
expect(subject.content.length).to eq(described_class::MAX_CONTENT_LENGTH)
end
end
context 'when message is not present' do
let(:content) { '' }
it 'returns an error' do
expect(subject.save).to be_falsey
expect(subject.errors[:content]).to be_present
end
end
context 'when message content is valid' do
let(:content) { 'valid message content' }
it 'is saved with default error severity' do
subject.save!
expect(subject.content).to eq(content)
expect(subject.severity).to eq('error')
expect(subject).to be_error
end
it 'is persist the defined severity' do
subject.severity = :warning
subject.save!
expect(subject.content).to eq(content)
expect(subject.severity).to eq('warning')
expect(subject).to be_warning
end
end
end
end

View File

@ -2634,6 +2634,28 @@ RSpec.describe Ci::Pipeline, :mailer do
end
end
describe '#add_error_message' do
let(:pipeline) { build_stubbed(:ci_pipeline) }
it 'adds a new pipeline error message' do
pipeline.add_error_message('The error message')
expect(pipeline.messages.map(&:content)).to contain_exactly('The error message')
end
context 'when feature flag ci_store_pipeline_messages is disabled' do
before do
stub_feature_flags(ci_store_pipeline_messages: false)
end
it ' does not add pipeline error message' do
pipeline.add_error_message('The error message')
expect(pipeline.messages).to be_empty
end
end
end
describe '#has_yaml_errors?' do
context 'when yaml_errors is set' do
before do
@ -3198,8 +3220,8 @@ RSpec.describe Ci::Pipeline, :mailer do
end
end
describe '#error_messages' do
subject { pipeline.error_messages }
describe '#full_error_messages' do
subject { pipeline.full_error_messages }
before do
pipeline.valid?

View File

@ -0,0 +1,54 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabIssueTrackerService do
describe "Associations" do
it { is_expected.to belong_to :project }
it { is_expected.to have_one :service_hook }
end
describe 'Validations' do
context 'when service is active' do
subject { described_class.new(project: create(:project), active: true) }
it { is_expected.to validate_presence_of(:issues_url) }
it_behaves_like 'issue tracker service URL attribute', :issues_url
end
context 'when service is inactive' do
subject { described_class.new(project: create(:project), active: false) }
it { is_expected.not_to validate_presence_of(:issues_url) }
end
end
describe 'project and issue urls' do
let(:project) { create(:project) }
let(:service) { project.create_gitlab_issue_tracker_service(active: true) }
context 'with absolute urls' do
before do
allow(described_class).to receive(:default_url_options).and_return(script_name: "/gitlab/root")
end
it 'gives the correct path' do
expect(service.project_url).to eq("http://#{Gitlab.config.gitlab.host}/gitlab/root/#{project.full_path}/-/issues")
expect(service.new_issue_url).to eq("http://#{Gitlab.config.gitlab.host}/gitlab/root/#{project.full_path}/-/issues/new")
expect(service.issue_url(432)).to eq("http://#{Gitlab.config.gitlab.host}/gitlab/root/#{project.full_path}/-/issues/432")
end
end
context 'with relative urls' do
before do
allow(described_class).to receive(:default_url_options).and_return(script_name: "/gitlab/root")
end
it 'gives the correct path' do
expect(service.issue_tracker_path).to eq("/gitlab/root/#{project.full_path}/-/issues")
expect(service.new_issue_path).to eq("/gitlab/root/#{project.full_path}/-/issues/new")
expect(service.issue_path(432)).to eq("/gitlab/root/#{project.full_path}/-/issues/432")
end
end
end
end

View File

@ -61,6 +61,7 @@ RSpec.describe Project do
it { is_expected.to have_one(:youtrack_service) }
it { is_expected.to have_one(:custom_issue_tracker_service) }
it { is_expected.to have_one(:bugzilla_service) }
it { is_expected.to have_one(:gitlab_issue_tracker_service) }
it { is_expected.to have_one(:external_wiki_service) }
it { is_expected.to have_one(:project_feature) }
it { is_expected.to have_one(:project_repository) }

View File

@ -508,7 +508,7 @@ RSpec.describe Service do
describe 'initialize service with no properties' do
let(:service) do
BugzillaService.create(
GitlabIssueTrackerService.create(
project: create(:project),
project_url: 'http://gitlab.example.com'
)

View File

@ -154,7 +154,7 @@ RSpec.describe GroupPolicy do
context 'admin' do
let(:current_user) { admin }
it do
specify do
expect_allowed(*read_group_permissions)
expect_allowed(*guest_permissions)
expect_allowed(*reporter_permissions)
@ -162,6 +162,10 @@ RSpec.describe GroupPolicy do
expect_allowed(*maintainer_permissions)
expect_allowed(*owner_permissions)
end
context 'with admin mode', :enable_admin_mode do
specify { expect_allowed(*admin_permissions) }
end
end
describe 'private nested group use the highest access level from the group and inherited permissions' do

View File

@ -30,7 +30,7 @@ RSpec.describe ProjectPolicy do
admin_issue admin_label admin_list read_commit_status read_build
read_container_image read_pipeline read_environment read_deployment
read_merge_request download_wiki_code read_sentry_issue read_metrics_dashboard_annotation
metrics_dashboard
metrics_dashboard read_confidential_issues
]
end

View File

@ -254,7 +254,10 @@ RSpec.describe API::Projects do
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.first).to include 'statistics'
statistics = json_response.first['statistics']
expect(statistics).to be_present
expect(statistics).to include('commit_count', 'storage_size', 'repository_size', 'wiki_size', 'lfs_objects_size', 'job_artifacts_size', 'snippets_size')
end
it "does not include license by default" do

View File

@ -194,6 +194,7 @@ RSpec.describe Ci::CreatePipelineService do
expect(head_pipeline).to be_persisted
expect(head_pipeline.yaml_errors).to be_present
expect(head_pipeline.messages).to be_present
expect(merge_request.reload.head_pipeline).to eq head_pipeline
end
end
@ -1695,6 +1696,7 @@ RSpec.describe Ci::CreatePipelineService do
expect(pipeline).to be_persisted
expect(pipeline.builds).to be_empty
expect(pipeline.yaml_errors).to eq("test_a: needs 'build_a'")
expect(pipeline.messages.pluck(:content)).to contain_exactly("test_a: needs 'build_a'")
expect(pipeline.errors[:base]).to contain_exactly("test_a: needs 'build_a'")
end
end
@ -1706,6 +1708,7 @@ RSpec.describe Ci::CreatePipelineService do
expect(pipeline).not_to be_persisted
expect(pipeline.builds).to be_empty
expect(pipeline.yaml_errors).to be_nil
expect(pipeline.messages).not_to be_empty
expect(pipeline.errors[:base]).to contain_exactly("test_a: needs 'build_a'")
end
end

View File

@ -38,6 +38,7 @@ RSpec.shared_context 'GroupPolicy context' do
:update_default_branch_protection
].compact
end
let(:admin_permissions) { %i[read_confidential_issues] }
before_all do
group.add_guest(guest)

View File

@ -1,7 +1,12 @@
# frozen_string_literal: true
RSpec.shared_examples 'resolving an issuable in GraphQL' do |type|
subject { mutation.resolve_issuable(type: type, parent_path: parent.full_path, iid: issuable.iid) }
include GraphqlHelpers
let(:parent_path) { parent.full_path }
let(:iid) { issuable.iid }
subject(:result) { mutation.resolve_issuable(type: type, parent_path: parent_path, iid: iid) }
context 'when user has access' do
before do
@ -9,37 +14,23 @@ RSpec.shared_examples 'resolving an issuable in GraphQL' do |type|
end
it 'resolves issuable by iid' do
result = type == :merge_request ? subject.sync : subject
expect(result).to eq(issuable)
end
it 'uses the correct Resolver to resolve issuable' do
resolver_class = "Resolvers::#{type.to_s.classify.pluralize}Resolver".constantize
resolve_method = type == :epic ? :resolve_group : :resolve_project
resolved_parent = mutation.send(resolve_method, full_path: parent.full_path)
allow(mutation).to receive(resolve_method)
.with(full_path: parent.full_path)
.and_return(resolved_parent)
expect(resolver_class.single).to receive(:new)
.with(object: resolved_parent, context: context, field: nil)
.and_call_original
subject
end
it 'returns nil if issuable is not found' do
result = mutation.resolve_issuable(type: type, parent_path: parent.full_path, iid: "100")
result = result.respond_to?(:sync) ? result.sync : result
context 'the IID does not refer to a valid issuable' do
let(:iid) { '100' }
it 'returns nil' do
expect(result).to be_nil
end
end
it 'returns nil if parent path is not present' do
result = mutation.resolve_issuable(type: type, parent_path: "", iid: issuable.iid)
context 'the parent path is not present' do
let(:parent_path) { '' }
it 'returns nil' do
expect(result).to be_nil
end
end
end
end

View File

@ -0,0 +1,34 @@
# frozen_string_literal: true
RSpec.shared_examples 'update namespace limit policy' do
describe 'update_subscription_limit' do
using RSpec::Parameterized::TableSyntax
let(:policy) { :update_subscription_limit }
where(:role, :is_com, :allowed) do
:user | true | false
:owner | true | false
:admin | true | true
:user | false | false
:owner | false | false
:admin | false | false
end
with_them do
let(:current_user) { build_stubbed(role) }
before do
allow(Gitlab).to receive(:com?).and_return(is_com)
end
context 'when admin mode enabled', :enable_admin_mode do
it { is_expected.to(allowed ? be_allowed(policy) : be_disallowed(policy)) }
end
context 'when admin mode disabled' do
it { is_expected.to be_disallowed(policy) }
end
end
end
end