Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2019-09-24 18:06:05 +00:00
parent f1a5755898
commit 2ed368929a
58 changed files with 720 additions and 123 deletions

View File

@ -1,5 +1,6 @@
extends: extends:
- '@gitlab' - '@gitlab'
- plugin:promise/recommended
globals: globals:
__webpack_public_path__: true __webpack_public_path__: true
gl: false gl: false
@ -42,6 +43,11 @@ rules:
no-jquery/no-load: error no-jquery/no-load: error
no-jquery/no-load-shorthand: error no-jquery/no-load-shorthand: error
no-jquery/no-serialize: error no-jquery/no-serialize: error
promise/always-return: off
promise/no-callback-in-promise: off
promise/no-nesting: off
promise/param-names: off
promise/valid-params: off
overrides: overrides:
files: files:
- '**/spec/**/*' - '**/spec/**/*'

View File

@ -100,6 +100,7 @@
refs: refs:
- master - master
- /^\d+-\d+-auto-deploy-\d+$/ - /^\d+-\d+-auto-deploy-\d+$/
- /^[\d-]+-stable(-ee)?$/
.only-review-schedules: .only-review-schedules:
only: only:

View File

@ -1,8 +1,15 @@
.except-deploys:
except:
refs:
- /^[\d-]+-stable(-ee)?$/
- /^\d+-\d+-auto-deploy-\d+$/
.review-docker: .review-docker:
extends: extends:
- .default-tags - .default-tags
- .default-retry - .default-retry
- .default-only - .default-only
- .except-deploys
image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-qa-alpine image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-qa-alpine
services: services:
- docker:19.03.0-dind - docker:19.03.0-dind
@ -36,6 +43,7 @@ schedule:review-cleanup:
- .default-only - .default-only
- .only-code-qa-changes - .only-code-qa-changes
- .only-review-schedules - .only-review-schedules
- .except-deploys
stage: prepare stage: prepare
image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-charts-build-base image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-charts-build-base
allow_failure: true allow_failure: true
@ -52,6 +60,7 @@ schedule:review-cleanup:
extends: extends:
- .default-only - .default-only
- .only-code-qa-changes - .only-code-qa-changes
- .except-deploys
image: ruby:2.6-alpine image: ruby:2.6-alpine
stage: review-prepare stage: review-prepare
before_script: before_script:
@ -80,6 +89,7 @@ schedule:review-build-cng:
- .default-retry - .default-retry
- .default-only - .default-only
- .only-code-qa-changes - .only-code-qa-changes
- .except-deploys
stage: review stage: review
image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-charts-build-base image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-charts-build-base
dependencies: [] dependencies: []
@ -257,6 +267,7 @@ parallel-spec-reports:
- .default-only - .default-only
- .only-code-qa-changes - .only-code-qa-changes
- .only-review - .only-review
- .except-deploys
image: ruby:2.6-alpine image: ruby:2.6-alpine
stage: post-test stage: post-test
dependencies: ["review-qa-all"] dependencies: ["review-qa-all"]

View File

@ -1,5 +1,8 @@
import Vue from 'vue'; import Vue from 'vue';
import GlFeatureFlagsPlugin from '~/vue_shared/gl_feature_flags_plugin';
if (process.env.NODE_ENV !== 'production') { if (process.env.NODE_ENV !== 'production') {
Vue.config.productionTip = false; Vue.config.productionTip = false;
} }
Vue.use(GlFeatureFlagsPlugin);

View File

@ -175,7 +175,6 @@ export default {
'metricsWithData', 'metricsWithData',
'useDashboardEndpoint', 'useDashboardEndpoint',
'allDashboards', 'allDashboards',
'multipleDashboardsEnabled',
'additionalPanelTypesEnabled', 'additionalPanelTypesEnabled',
]), ]),
firstDashboard() { firstDashboard() {
@ -318,7 +317,6 @@ export default {
<div class="row"> <div class="row">
<template v-if="environmentsEndpoint"> <template v-if="environmentsEndpoint">
<gl-form-group <gl-form-group
v-if="multipleDashboardsEnabled"
:label="__('Dashboard')" :label="__('Dashboard')"
label-size="sm" label-size="sm"
label-for="monitor-dashboards-dropdown" label-for="monitor-dashboards-dropdown"

View File

@ -14,7 +14,6 @@ export default (props = {}) => {
if (gon.features) { if (gon.features) {
store.dispatch('monitoringDashboard/setFeatureFlags', { store.dispatch('monitoringDashboard/setFeatureFlags', {
prometheusEndpointEnabled: gon.features.environmentMetricsUsePrometheusEndpoint, prometheusEndpointEnabled: gon.features.environmentMetricsUsePrometheusEndpoint,
multipleDashboardsEnabled: gon.features.environmentMetricsShowMultipleDashboards,
additionalPanelTypesEnabled: gon.features.environmentMetricsAdditionalPanelTypes, additionalPanelTypesEnabled: gon.features.environmentMetricsAdditionalPanelTypes,
}); });
} }

View File

@ -37,10 +37,9 @@ export const setEndpoints = ({ commit }, endpoints) => {
export const setFeatureFlags = ( export const setFeatureFlags = (
{ commit }, { commit },
{ prometheusEndpointEnabled, multipleDashboardsEnabled, additionalPanelTypesEnabled }, { prometheusEndpointEnabled, additionalPanelTypesEnabled },
) => { ) => {
commit(types.SET_DASHBOARD_ENABLED, prometheusEndpointEnabled); commit(types.SET_DASHBOARD_ENABLED, prometheusEndpointEnabled);
commit(types.SET_MULTIPLE_DASHBOARDS_ENABLED, multipleDashboardsEnabled);
commit(types.SET_ADDITIONAL_PANEL_TYPES_ENABLED, additionalPanelTypesEnabled); commit(types.SET_ADDITIONAL_PANEL_TYPES_ENABLED, additionalPanelTypesEnabled);
}; };
@ -51,13 +50,8 @@ export const setShowErrorBanner = ({ commit }, enabled) => {
export const requestMetricsDashboard = ({ commit }) => { export const requestMetricsDashboard = ({ commit }) => {
commit(types.REQUEST_METRICS_DATA); commit(types.REQUEST_METRICS_DATA);
}; };
export const receiveMetricsDashboardSuccess = ( export const receiveMetricsDashboardSuccess = ({ commit, dispatch }, { response, params }) => {
{ state, commit, dispatch }, commit(types.SET_ALL_DASHBOARDS, response.all_dashboards);
{ response, params },
) => {
if (state.multipleDashboardsEnabled) {
commit(types.SET_ALL_DASHBOARDS, response.all_dashboards);
}
commit(types.RECEIVE_METRICS_DATA_SUCCESS, response.dashboard.panel_groups); commit(types.RECEIVE_METRICS_DATA_SUCCESS, response.dashboard.panel_groups);
dispatch('fetchPrometheusMetrics', params); dispatch('fetchPrometheusMetrics', params);
}; };

View File

@ -10,7 +10,6 @@ export const RECEIVE_ENVIRONMENTS_DATA_FAILURE = 'RECEIVE_ENVIRONMENTS_DATA_FAIL
export const SET_QUERY_RESULT = 'SET_QUERY_RESULT'; export const SET_QUERY_RESULT = 'SET_QUERY_RESULT';
export const SET_TIME_WINDOW = 'SET_TIME_WINDOW'; export const SET_TIME_WINDOW = 'SET_TIME_WINDOW';
export const SET_DASHBOARD_ENABLED = 'SET_DASHBOARD_ENABLED'; export const SET_DASHBOARD_ENABLED = 'SET_DASHBOARD_ENABLED';
export const SET_MULTIPLE_DASHBOARDS_ENABLED = 'SET_MULTIPLE_DASHBOARDS_ENABLED';
export const SET_ADDITIONAL_PANEL_TYPES_ENABLED = 'SET_ADDITIONAL_PANEL_TYPES_ENABLED'; export const SET_ADDITIONAL_PANEL_TYPES_ENABLED = 'SET_ADDITIONAL_PANEL_TYPES_ENABLED';
export const SET_ALL_DASHBOARDS = 'SET_ALL_DASHBOARDS'; export const SET_ALL_DASHBOARDS = 'SET_ALL_DASHBOARDS';
export const SET_ENDPOINTS = 'SET_ENDPOINTS'; export const SET_ENDPOINTS = 'SET_ENDPOINTS';

View File

@ -89,9 +89,6 @@ export default {
[types.SET_DASHBOARD_ENABLED](state, enabled) { [types.SET_DASHBOARD_ENABLED](state, enabled) {
state.useDashboardEndpoint = enabled; state.useDashboardEndpoint = enabled;
}, },
[types.SET_MULTIPLE_DASHBOARDS_ENABLED](state, enabled) {
state.multipleDashboardsEnabled = enabled;
},
[types.SET_GETTING_STARTED_EMPTY_STATE](state) { [types.SET_GETTING_STARTED_EMPTY_STATE](state) {
state.emptyState = 'gettingStarted'; state.emptyState = 'gettingStarted';
}, },

View File

@ -8,7 +8,6 @@ export default () => ({
deploymentsEndpoint: null, deploymentsEndpoint: null,
dashboardEndpoint: invalidUrl, dashboardEndpoint: invalidUrl,
useDashboardEndpoint: false, useDashboardEndpoint: false,
multipleDashboardsEnabled: false,
additionalPanelTypesEnabled: false, additionalPanelTypesEnabled: false,
emptyState: 'gettingStarted', emptyState: 'gettingStarted',
showEmptyState: true, showEmptyState: true,

View File

@ -7,6 +7,7 @@ import _ from 'underscore';
import axios from './lib/utils/axios_utils'; import axios from './lib/utils/axios_utils';
import { s__, __, sprintf } from './locale'; import { s__, __, sprintf } from './locale';
import ModalStore from './boards/stores/modal_store'; import ModalStore from './boards/stores/modal_store';
import { parseBoolean } from './lib/utils/common_utils';
// TODO: remove eventHub hack after code splitting refactor // TODO: remove eventHub hack after code splitting refactor
window.emitSidebarEvent = window.emitSidebarEvent || $.noop; window.emitSidebarEvent = window.emitSidebarEvent || $.noop;
@ -275,12 +276,13 @@ function UsersSelect(currentUser, els, options = {}) {
}) })
.map(input => { .map(input => {
const userId = parseInt(input.value, 10); const userId = parseInt(input.value, 10);
const { avatarUrl, avatar_url, name, username } = input.dataset; const { avatarUrl, avatar_url, name, username, canMerge } = input.dataset;
return { return {
avatar_url: avatarUrl || avatar_url, avatar_url: avatarUrl || avatar_url,
id: userId, id: userId,
name, name,
username, username,
can_merge: parseBoolean(canMerge),
}; };
}); });

View File

@ -1,5 +1,7 @@
<script> <script>
import $ from 'jquery'; import $ from 'jquery';
import { __ } from '~/locale';
import createFlash from '~/flash';
import statusIcon from '../mr_widget_status_icon.vue'; import statusIcon from '../mr_widget_status_icon.vue';
import tooltip from '../../../vue_shared/directives/tooltip'; import tooltip from '../../../vue_shared/directives/tooltip';
import eventHub from '../../event_hub'; import eventHub from '../../event_hub';
@ -29,12 +31,12 @@ export default {
.then(res => res.data) .then(res => res.data)
.then(data => { .then(data => {
eventHub.$emit('UpdateWidgetData', data); eventHub.$emit('UpdateWidgetData', data);
new window.Flash('The merge request can now be merged.', 'notice'); // eslint-disable-line createFlash(__('The merge request can now be merged.'), 'notice');
$('.merge-request .detail-page-description .title').text(this.mr.title); $('.merge-request .detail-page-description .title').text(this.mr.title);
}) })
.catch(() => { .catch(() => {
this.isMakingRequest = false; this.isMakingRequest = false;
new window.Flash('Something went wrong. Please try again.'); // eslint-disable-line createFlash(__('Something went wrong. Please try again.'));
}); });
}, },
}, },

View File

@ -0,0 +1,7 @@
export default Vue => {
Vue.mixin({
provide: {
glFeatures: { ...((window.gon && window.gon.features) || {}) },
},
});
};

View File

@ -0,0 +1,8 @@
export default () => ({
inject: {
glFeatures: {
from: 'glFeatures',
default: () => ({}),
},
},
});

View File

@ -245,14 +245,6 @@ $note-form-margin-left: 72px;
} }
} }
.note-header {
@include notes-media('max', map-get($grid-breakpoints, xs)) {
.inline {
display: block;
}
}
}
.note-emoji-button { .note-emoji-button {
position: relative; position: relative;
line-height: 1; line-height: 1;
@ -635,10 +627,6 @@ $note-form-margin-left: 72px;
.note-headline-light { .note-headline-light {
display: inline; display: inline;
@include notes-media('max', map-get($grid-breakpoints, xs)) {
display: block;
}
} }
.note-headline-light, .note-headline-light,

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
module RendersAssignees
def preload_assignees_for_render(merge_request)
merge_request.project.team.max_member_access_for_user_ids(merge_request.assignees.map(&:id))
end
end

View File

@ -12,7 +12,6 @@ class Projects::EnvironmentsController < Projects::ApplicationController
before_action :expire_etag_cache, only: [:index] before_action :expire_etag_cache, only: [:index]
before_action only: [:metrics, :additional_metrics, :metrics_dashboard] do before_action only: [:metrics, :additional_metrics, :metrics_dashboard] do
push_frontend_feature_flag(:environment_metrics_use_prometheus_endpoint, default_enabled: true) push_frontend_feature_flag(:environment_metrics_use_prometheus_endpoint, default_enabled: true)
push_frontend_feature_flag(:environment_metrics_show_multiple_dashboards)
push_frontend_feature_flag(:environment_metrics_additional_panel_types) push_frontend_feature_flag(:environment_metrics_additional_panel_types)
push_frontend_feature_flag(:prometheus_computed_alerts) push_frontend_feature_flag(:prometheus_computed_alerts)
end end
@ -168,7 +167,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
dashboard_path: params[:dashboard], dashboard_path: params[:dashboard],
**dashboard_params.to_h.symbolize_keys **dashboard_params.to_h.symbolize_keys
) )
elsif Feature.enabled?(:environment_metrics_show_multiple_dashboards, project) else
result = dashboard_finder.find( result = dashboard_finder.find(
project, project,
current_user, current_user,
@ -177,8 +176,6 @@ class Projects::EnvironmentsController < Projects::ApplicationController
) )
result[:all_dashboards] = dashboard_finder.find_all_paths(project) result[:all_dashboards] = dashboard_finder.find_all_paths(project)
else
result = dashboard_finder.find(project, current_user, environment: environment)
end end
respond_to do |format| respond_to do |format|

View File

@ -5,6 +5,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
include IssuableActions include IssuableActions
include RendersNotes include RendersNotes
include RendersCommits include RendersCommits
include RendersAssignees
include ToggleAwardEmoji include ToggleAwardEmoji
include IssuableCollections include IssuableCollections
include RecordUserLastActivity include RecordUserLastActivity
@ -41,6 +42,8 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
# use next to appease Rubocop # use next to appease Rubocop
next render('invalid') if target_branch_missing? next render('invalid') if target_branch_missing?
preload_assignees_for_render(@merge_request)
# Build a note object for comment form # Build a note object for comment form
@note = @project.notes.new(noteable: @merge_request) @note = @project.notes.new(noteable: @merge_request)

View File

@ -372,6 +372,12 @@ module IssuablesHelper
finder.class.scalar_params.any? { |p| params[p].present? } finder.class.scalar_params.any? { |p| params[p].present? }
end end
def assignee_sidebar_data(assignee, merge_request: nil)
{ avatar_url: assignee.avatar_url, name: assignee.name, username: assignee.username }.tap do |data|
data[:can_merge] = merge_request.can_be_merged_by?(assignee) if merge_request
end
end
private private
def sidebar_gutter_collapsed? def sidebar_gutter_collapsed?

View File

@ -40,7 +40,7 @@
.info-well .info-well
.well-segment.admin-well.admin-well-features .well-segment.admin-well.admin-well-features
%h4 Features %h4 Features
= feature_entry(_('Sign up'), href: admin_application_settings_path(anchor: 'js-signup-settings')) = feature_entry(_('Sign up'), href: admin_application_settings_path(anchor: 'js-signup-settings'), enabled: allow_signup?)
= feature_entry(_('LDAP'), enabled: Gitlab.config.ldap.enabled) = feature_entry(_('LDAP'), enabled: Gitlab.config.ldap.enabled)
= feature_entry(_('Gravatar'), href: admin_application_settings_path(anchor: 'js-account-settings'), enabled: gravatar_enabled?) = feature_entry(_('Gravatar'), href: admin_application_settings_path(anchor: 'js-account-settings'), enabled: gravatar_enabled?)
= feature_entry(_('OmniAuth'), href: admin_application_settings_path(anchor: 'js-signin-settings'), enabled: Gitlab::Auth.omniauth_enabled?) = feature_entry(_('OmniAuth'), href: admin_application_settings_path(anchor: 'js-signin-settings'), enabled: Gitlab::Auth.omniauth_enabled?)

View File

@ -11,7 +11,7 @@
= hidden_field_tag "#{issuable_type}[assignee_ids][]", 0, id: nil = hidden_field_tag "#{issuable_type}[assignee_ids][]", 0, id: nil
- else - else
- assignees.each do |assignee| - assignees.each do |assignee|
= hidden_field_tag "#{issuable_type}[assignee_ids][]", assignee.id, id: nil, data: { avatar_url: assignee.avatar_url, name: assignee.name, username: assignee.username } = hidden_field_tag "#{issuable_type}[assignee_ids][]", assignee.id, id: nil, data: assignee_sidebar_data(assignee, merge_request: @merge_request)
- options = { toggle_class: 'js-user-search js-author-search', - options = { toggle_class: 'js-user-search js-author-search',
title: _('Assign to'), title: _('Assign to'),

View File

@ -0,0 +1,5 @@
---
title: Remove map-get($grid-breakpoints, xs) for max-width
merge_request: 17420
author: Takuya Noguchi
type: other

View File

@ -0,0 +1,5 @@
---
title: Fix cannot merge icon showing in dropdown for users who can merge
merge_request: 17306
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Fix signup link in admin area not being disabled
merge_request: 16726
author: Illya Klymov
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Add allowFilter and allowAnySHA1InWant for partial clones
merge_request: 16850
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Add timeout mechanism for CI config validation
merge_request: 16807
author:
type: fixed

View File

@ -2289,6 +2289,10 @@ Nested includes allow you to compose a set of includes.
A total of 50 includes is allowed. A total of 50 includes is allowed.
Duplicate includes are considered a configuration error. Duplicate includes are considered a configuration error.
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/28212) in GitLab 12.4.
A hard limit of 30 seconds was set for resolving all files.
#### `include` examples #### `include` examples
Here are a few more `include` examples. Here are a few more `include` examples.

View File

@ -104,6 +104,51 @@ document.addEventListener('DOMContentLoaded', () => new Vue({
})); }));
``` ```
#### Accessing feature flags
Use Vue's [provide/inject](https://vuejs.org/v2/api/#provide-inject) mechanism
to make feature flags available to any descendant components in a Vue
application. The `glFeatures` object is already provided in `commons/vue.js`, so
only the mixin is required to utilize the flags:
```javascript
// An arbitrary descendant component
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
// ...
mixins: [glFeatureFlagsMixin()],
// ...
created() {
if (this.glFeatures.myFlag) {
// ...
}
},
}
```
This approach has a few benefits:
- Arbitrarily deeply nested components can opt-in and access the flag without
intermediate components being aware of it (c.f. passing the flag down via
props).
- Good testability, since the flag can be provided to `mount`/`shallowMount`
from `vue-test-utils` as easily as a prop.
```javascript
import { shallowMount } from '@vue/test-utils';
shallowMount(component, {
provide: {
glFeatures: { myFlag: true },
},
});
```
- No need to access a global variable, except in the application's
[entry point](#accessing-the-gl-object).
### A folder for Components ### A folder for Components
This folder holds all components that are specific of this new feature. This folder holds all components that are specific of this new feature.

View File

@ -109,6 +109,9 @@ if ( gon.features.vimBindings ) {
The name of the feature flag in JavaScript will always be camelCased, meaning The name of the feature flag in JavaScript will always be camelCased, meaning
that checking for `gon.features.vim_bindings` would not work. that checking for `gon.features.vim_bindings` would not work.
See the [Vue guide](../fe_guide/vue.md#accessing-feature-flags) for details about
how to access feature flags in a Vue component.
### Specs ### Specs
In the test environment `Feature.enabled?` is stubbed to always respond to `true`, In the test environment `Feature.enabled?` is stubbed to always respond to `true`,

View File

@ -78,6 +78,10 @@ module API
receive_max_input_size = Gitlab::CurrentSettings.receive_max_input_size.to_i receive_max_input_size = Gitlab::CurrentSettings.receive_max_input_size.to_i
if receive_max_input_size > 0 if receive_max_input_size > 0
payload[:git_config_options] << "receive.maxInputSize=#{receive_max_input_size.megabytes}" payload[:git_config_options] << "receive.maxInputSize=#{receive_max_input_size.megabytes}"
if Feature.enabled?(:gitaly_upload_pack_filter, project)
payload[:git_config_options] << "uploadpack.allowFilter=true" << "uploadpack.allowAnySHA1InWant=true"
end
end end
response_with_status(**payload) response_with_status(**payload)

View File

@ -7,6 +7,8 @@ module Gitlab
# #
class Config class Config
ConfigError = Class.new(StandardError) ConfigError = Class.new(StandardError)
TIMEOUT_SECONDS = 30.seconds
TIMEOUT_MESSAGE = 'Resolving config took longer than expected'
RESCUE_ERRORS = [ RESCUE_ERRORS = [
Gitlab::Config::Loader::FormatError, Gitlab::Config::Loader::FormatError,
@ -17,17 +19,17 @@ module Gitlab
attr_reader :root attr_reader :root
def initialize(config, project: nil, sha: nil, user: nil) def initialize(config, project: nil, sha: nil, user: nil)
@config = Config::Extendable @context = build_context(project: project, sha: sha, user: user)
.new(build_config(config, project: project, sha: sha, user: user))
.to_hash if Feature.enabled?(:ci_limit_yaml_expansion, project, default_enabled: true)
@context.set_deadline(TIMEOUT_SECONDS)
end
@config = expand_config(config)
@root = Entry::Root.new(@config) @root = Entry::Root.new(@config)
@root.compose! @root.compose!
rescue Gitlab::Config::Loader::Yaml::DataTooLargeError => e
Gitlab::Sentry.track_exception(e, extra: { user: user.inspect, project: project.inspect })
raise Config::ConfigError, e.message
rescue *rescue_errors => e rescue *rescue_errors => e
raise Config::ConfigError, e.message raise Config::ConfigError, e.message
end end
@ -61,18 +63,34 @@ module Gitlab
private private
def build_config(config, project:, sha:, user:) def expand_config(config)
initial_config = Gitlab::Config::Loader::Yaml.new(config).load! build_config(config)
process_external_files(initial_config, project: project, sha: sha, user: user) rescue Gitlab::Config::Loader::Yaml::DataTooLargeError => e
track_exception(e)
raise Config::ConfigError, e.message
rescue Gitlab::Ci::Config::External::Context::TimeoutError => e
track_exception(e)
raise Config::ConfigError, TIMEOUT_MESSAGE
end end
def process_external_files(config, project:, sha:, user:) def build_config(config)
Config::External::Processor.new(config, initial_config = Gitlab::Config::Loader::Yaml.new(config).load!
initial_config = Config::External::Processor.new(initial_config, @context).perform
Config::Extendable.new(initial_config).to_hash
end
def build_context(project:, sha:, user:)
Config::External::Context.new(
project: project, project: project,
sha: sha || project&.repository&.root_ref_sha, sha: sha || project&.repository&.root_ref_sha,
user: user, user: user)
expandset: Set.new).perform end
def track_exception(error)
Gitlab::Sentry.track_exception(error, extra: @context.sentry_payload)
end end
# Overriden in EE # Overriden in EE

View File

@ -0,0 +1,64 @@
# frozen_string_literal: true
module Gitlab
module Ci
class Config
module External
class Context
TimeoutError = Class.new(StandardError)
attr_reader :project, :sha, :user
attr_reader :expandset, :execution_deadline
def initialize(project: nil, sha: nil, user: nil)
@project = project
@sha = sha
@user = user
@expandset = Set.new
@execution_deadline = 0
yield self if block_given?
end
def mutate(attrs = {})
self.class.new(**attrs) do |ctx|
ctx.expandset = expandset
ctx.execution_deadline = execution_deadline
end
end
def set_deadline(timeout_seconds)
@execution_deadline = current_monotonic_time + timeout_seconds.to_f
end
def check_execution_time!
raise TimeoutError if execution_expired?
end
def sentry_payload
{
user: user.inspect,
project: project.inspect
}
end
protected
attr_writer :expandset, :execution_deadline
private
def current_monotonic_time
Gitlab::Metrics::System.monotonic_time
end
def execution_expired?
return false if execution_deadline.zero?
current_monotonic_time > execution_deadline
end
end
end
end
end
end

View File

@ -12,8 +12,6 @@ module Gitlab
YAML_WHITELIST_EXTENSION = /.+\.(yml|yaml)$/i.freeze YAML_WHITELIST_EXTENSION = /.+\.(yml|yaml)$/i.freeze
Context = Struct.new(:project, :sha, :user, :expandset)
def initialize(params, context) def initialize(params, context)
@params = params @params = params
@context = context @context = context
@ -69,11 +67,16 @@ module Gitlab
end end
def validate! def validate!
validate_execution_time!
validate_location! validate_location!
validate_content! if errors.none? validate_content! if errors.none?
validate_hash! if errors.none? validate_hash! if errors.none?
end end
def validate_execution_time!
context.check_execution_time!
end
def validate_location! def validate_location!
if invalid_location_type? if invalid_location_type?
errors.push("Included file `#{location}` needs to be a string") errors.push("Included file `#{location}` needs to be a string")
@ -95,11 +98,11 @@ module Gitlab
end end
def expand_includes(hash) def expand_includes(hash)
External::Processor.new(hash, **expand_context).perform External::Processor.new(hash, context.mutate(expand_context_attrs)).perform
end end
def expand_context def expand_context_attrs
{ project: nil, sha: nil, user: nil, expandset: context.expandset } {}
end end
end end
end end

View File

@ -6,6 +6,7 @@ module Gitlab
module External module External
module File module File
class Local < Base class Local < Base
extend ::Gitlab::Utils::Override
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
def initialize(params, context) def initialize(params, context)
@ -34,11 +35,13 @@ module Gitlab
context.project.repository.blob_data_at(context.sha, location) context.project.repository.blob_data_at(context.sha, location)
end end
def expand_context override :expand_context_attrs
super.merge( def expand_context_attrs
{
project: context.project, project: context.project,
sha: context.sha, sha: context.sha,
user: context.user) user: context.user
}
end end
end end
end end

View File

@ -6,11 +6,12 @@ module Gitlab
module External module External
module File module File
class Project < Base class Project < Base
extend ::Gitlab::Utils::Override
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
attr_reader :project_name, :ref_name attr_reader :project_name, :ref_name
def initialize(params, context = {}) def initialize(params, context)
@location = params[:file] @location = params[:file]
@project_name = params[:project] @project_name = params[:project]
@ref_name = params[:ref] || 'HEAD' @ref_name = params[:ref] || 'HEAD'
@ -65,11 +66,13 @@ module Gitlab
end end
end end
def expand_context override :expand_context_attrs
super.merge( def expand_context_attrs
{
project: project, project: project,
sha: sha, sha: sha,
user: context.user) user: context.user
}
end end
end end
end end

View File

@ -21,14 +21,9 @@ module Gitlab
DuplicateIncludesError = Class.new(Error) DuplicateIncludesError = Class.new(Error)
TooManyIncludesError = Class.new(Error) TooManyIncludesError = Class.new(Error)
def initialize(values, project:, sha:, user:, expandset:) def initialize(values, context)
raise Error, 'Expanded needs to be `Set`' unless expandset.is_a?(Set)
@locations = Array.wrap(values.fetch(:include, [])) @locations = Array.wrap(values.fetch(:include, []))
@project = project @context = context
@sha = sha
@user = user
@expandset = expandset
end end
def process def process
@ -43,7 +38,9 @@ module Gitlab
private private
attr_reader :locations, :project, :sha, :user, :expandset attr_reader :locations, :context
delegate :expandset, to: :context
# convert location if String to canonical form # convert location if String to canonical form
def normalize_location(location) def normalize_location(location)
@ -68,11 +65,11 @@ module Gitlab
end end
# We scope location to context, as this allows us to properly support # We scope location to context, as this allows us to properly support
# relative incldues, and similarly looking relative in another project # relative includes, and similarly looking relative in another project
# does not trigger duplicate error # does not trigger duplicate error
scoped_location = location.merge( scoped_location = location.merge(
context_project: project, context_project: context.project,
context_sha: sha) context_sha: context.sha)
unless expandset.add?(scoped_location) unless expandset.add?(scoped_location)
raise DuplicateIncludesError, "Include `#{location.to_json}` was already included!" raise DuplicateIncludesError, "Include `#{location.to_json}` was already included!"
@ -88,12 +85,6 @@ module Gitlab
matching.first matching.first
end end
def context
strong_memoize(:context) do
External::File::Base::Context.new(project, sha, user, expandset)
end
end
end end
end end
end end

View File

@ -7,9 +7,9 @@ module Gitlab
class Processor class Processor
IncludeError = Class.new(StandardError) IncludeError = Class.new(StandardError)
def initialize(values, project:, sha:, user:, expandset:) def initialize(values, context)
@values = values @values = values
@external_files = External::Mapper.new(values, project: project, sha: sha, user: user, expandset: expandset).process @external_files = External::Mapper.new(values, context).process
@content = {} @content = {}
rescue External::Mapper::Error, rescue External::Mapper::Error,
OpenSSL::SSL::SSLError => e OpenSSL::SSL::SSLError => e

View File

@ -15527,6 +15527,9 @@ msgstr ""
msgid "The merge conflicts for this merge request have already been resolved. Please return to the merge request." msgid "The merge conflicts for this merge request have already been resolved. Please return to the merge request."
msgstr "" msgstr ""
msgid "The merge request can now be merged."
msgstr ""
msgid "The name %{entryName} is already taken in this directory." msgid "The name %{entryName} is already taken in this directory."
msgstr "" msgstr ""

View File

@ -483,10 +483,8 @@ describe Projects::EnvironmentsController do
end end
shared_examples_for 'the default dashboard' do shared_examples_for 'the default dashboard' do
all_dashboards = Feature.enabled?(:environment_metrics_show_multiple_dashboards)
it_behaves_like '200 response' it_behaves_like '200 response'
it_behaves_like 'includes all dashboards' if all_dashboards it_behaves_like 'includes all dashboards'
it 'is the default dashboard' do it 'is the default dashboard' do
get :metrics_dashboard, params: environment_params(dashboard_params) get :metrics_dashboard, params: environment_params(dashboard_params)
@ -618,16 +616,6 @@ describe Projects::EnvironmentsController do
it_behaves_like 'the default dashboard' it_behaves_like 'the default dashboard'
it_behaves_like 'dashboard can be specified' it_behaves_like 'dashboard can be specified'
it_behaves_like 'dashboard can be embedded' it_behaves_like 'dashboard can be embedded'
context 'when multiple dashboards is disabled' do
before do
stub_feature_flags(environment_metrics_show_multiple_dashboards: false)
end
it_behaves_like 'the default dashboard'
it_behaves_like 'dashboard cannot be specified'
it_behaves_like 'dashboard can be embedded'
end
end end
describe 'GET #search' do describe 'GET #search' do

View File

@ -0,0 +1,68 @@
# frozen_string_literal: true
require 'spec_helper'
describe 'Merge request > User edits assignees sidebar', :js do
let(:project) { create(:project, :public, :repository) }
let(:protected_branch) { create(:protected_branch, :maintainers_can_push, name: 'master', project: project) }
let(:merge_request) { create(:merge_request, :simple, source_project: project, target_branch: protected_branch.name) }
let(:users_find_limit) { 5 }
# Insert more than limit so that response doesn't include assigned user
let(:project_developers) { Array.new(users_find_limit + 1) { create(:user).tap { |u| project.add_developer(u) } } }
let(:project_maintainers) { Array.new(users_find_limit + 1) { create(:user).tap { |u| project.add_maintainer(u) } } }
# DOM finders to simplify and improve readability
let(:sidebar_assignee_block) { page.find('.js-issuable-sidebar .assignee') }
let(:sidebar_assignee_avatar_link) { sidebar_assignee_block.find_all('a').find { |a| a['href'].include? assignee.username } }
let(:sidebar_assignee_tooltip) { sidebar_assignee_avatar_link['data-original-title'] || '' }
let(:sidebar_assignee_dropdown_item) { sidebar_assignee_block.find(".dropdown-menu li[data-user-id=\"#{assignee.id}\"]") }
let(:sidebar_assignee_dropdown_tooltip) { sidebar_assignee_dropdown_item.find('a')['data-title'] || '' }
before do
stub_const('Autocomplete::UsersFinder::LIMIT', users_find_limit)
sign_in(project.owner)
merge_request.assignees << assignee
visit project_merge_request_path(project, merge_request)
wait_for_requests
end
shared_examples 'when assigned' do |expected_tooltip: ''|
it 'shows assignee name' do
expect(sidebar_assignee_block).to have_text(assignee.name)
end
it "shows assignee tooltip '#{expected_tooltip}'" do
expect(sidebar_assignee_tooltip).to eql(expected_tooltip)
end
context 'when edit is clicked' do
before do
sidebar_assignee_block.click_link('Edit')
wait_for_requests
end
it "shows assignee tooltip '#{expected_tooltip}" do
expect(sidebar_assignee_dropdown_tooltip).to eql(expected_tooltip)
end
end
end
context 'when assigned to maintainer' do
let(:assignee) { project_maintainers.last }
it_behaves_like 'when assigned', expected_tooltip: ''
end
context 'when assigned to developer' do
let(:assignee) { project_developers.last }
it_behaves_like 'when assigned', expected_tooltip: 'Cannot merge'
end
end

View File

@ -0,0 +1,42 @@
import { createLocalVue, shallowMount } from '@vue/test-utils';
import GlFeatureFlags from '~/vue_shared/gl_feature_flags_plugin';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
const localVue = createLocalVue();
describe('GitLab Feature Flags Plugin', () => {
beforeEach(() => {
window.gon = {
features: {
aFeature: true,
bFeature: false,
},
};
localVue.use(GlFeatureFlags);
});
it('should provide glFeatures to components', () => {
const component = {
template: `<span></span>`,
inject: ['glFeatures'],
};
const wrapper = shallowMount(component, { localVue });
expect(wrapper.vm.glFeatures).toEqual({
aFeature: true,
bFeature: false,
});
});
it('should integrate with the glFeatureMixin', () => {
const component = {
template: `<span></span>`,
mixins: [glFeatureFlagsMixin()],
};
const wrapper = shallowMount(component, { localVue });
expect(wrapper.vm.glFeatures).toEqual({
aFeature: true,
bFeature: false,
});
});
});

View File

@ -0,0 +1,36 @@
import { createLocalVue, shallowMount } from '@vue/test-utils';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
const localVue = createLocalVue();
describe('GitLab Feature Flags Mixin', () => {
let wrapper;
beforeEach(() => {
const gon = {
features: {
aFeature: true,
bFeature: false,
},
};
const component = {
template: `<span></span>`,
mixins: [glFeatureFlagsMixin()],
};
wrapper = shallowMount(component, {
localVue,
provide: {
glFeatures: { ...(gon.features || {}) },
},
});
});
it('should provide glFeatures to components', () => {
expect(wrapper.vm.glFeatures).toEqual({
aFeature: true,
bFeature: false,
});
});
});

View File

@ -243,4 +243,32 @@ describe IssuablesHelper do
end end
end end
end end
describe '#assignee_sidebar_data' do
let(:user) { create(:user) }
let(:merge_request) { nil }
subject { helper.assignee_sidebar_data(user, merge_request: merge_request) }
it 'returns hash of assignee data' do
is_expected.to eql({
avatar_url: user.avatar_url,
name: user.name,
username: user.username
})
end
context 'with merge_request' do
let(:merge_request) { build_stubbed(:merge_request) }
where(can_merge: [true, false])
with_them do
before do
allow(merge_request).to receive(:can_be_merged_by?).and_return(can_merge)
end
it { is_expected.to include({ can_merge: can_merge })}
end
end
end
end end

View File

@ -4,6 +4,7 @@ import { createStore } from '~/create_cluster/gke_cluster/store';
import { SET_PROJECTS } from '~/create_cluster/gke_cluster/store/mutation_types'; import { SET_PROJECTS } from '~/create_cluster/gke_cluster/store/mutation_types';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { emptyProjectMock, selectedProjectMock } from '../mock_data'; import { emptyProjectMock, selectedProjectMock } from '../mock_data';
import { gapi } from '../helpers';
const componentConfig = { const componentConfig = {
docsUrl: 'https://console.cloud.google.com/home/dashboard', docsUrl: 'https://console.cloud.google.com/home/dashboard',
@ -32,6 +33,16 @@ describe('GkeProjectIdDropdown', () => {
let vm; let vm;
let store; let store;
let originalGapi;
beforeAll(() => {
originalGapi = window.gapi;
window.gapi = gapi();
});
afterAll(() => {
window.gapi = originalGapi;
});
beforeEach(() => { beforeEach(() => {
store = createStore(); store = createStore();
vm = createComponent(store); vm = createComponent(store);

View File

@ -64,7 +64,15 @@ describe('GCP Cluster Dropdown Store Actions', () => {
}); });
describe('async fetch methods', () => { describe('async fetch methods', () => {
window.gapi = gapi(); let originalGapi;
beforeAll(() => {
originalGapi = window.gapi;
window.gapi = gapi();
});
afterAll(() => {
window.gapi = originalGapi;
});
describe('fetchProjects', () => { describe('fetchProjects', () => {
it('fetches projects from Google API', done => { it('fetches projects from Google API', done => {

View File

@ -527,7 +527,6 @@ describe('Dashboard', () => {
component.$store.dispatch('monitoringDashboard/setFeatureFlags', { component.$store.dispatch('monitoringDashboard/setFeatureFlags', {
prometheusEndpoint: false, prometheusEndpoint: false,
multipleDashboardsEnabled: true,
}); });
component.$store.commit( component.$store.commit(

View File

@ -240,8 +240,6 @@ describe('Monitoring store actions', () => {
const response = metricsDashboardResponse; const response = metricsDashboardResponse;
response.all_dashboards = dashboardGitResponse; response.all_dashboards = dashboardGitResponse;
state.multipleDashboardsEnabled = true;
receiveMetricsDashboardSuccess({ state, commit, dispatch }, { response, params }); receiveMetricsDashboardSuccess({ state, commit, dispatch }, { response, params });
expect(commit).toHaveBeenCalledWith(types.SET_ALL_DASHBOARDS, dashboardGitResponse); expect(commit).toHaveBeenCalledWith(types.SET_ALL_DASHBOARDS, dashboardGitResponse);

View File

@ -47,7 +47,7 @@ describe('Wip', () => {
it('should make a request to service and handle response', done => { it('should make a request to service and handle response', done => {
const vm = createComponent(); const vm = createComponent();
spyOn(window, 'Flash').and.returnValue(true); const flashSpy = spyOnDependency(WorkInProgress, 'createFlash').and.returnValue(true);
spyOn(eventHub, '$emit'); spyOn(eventHub, '$emit');
spyOn(vm.service, 'removeWIP').and.returnValue( spyOn(vm.service, 'removeWIP').and.returnValue(
new Promise(resolve => { new Promise(resolve => {
@ -61,10 +61,7 @@ describe('Wip', () => {
setTimeout(() => { setTimeout(() => {
expect(vm.isMakingRequest).toBeTruthy(); expect(vm.isMakingRequest).toBeTruthy();
expect(eventHub.$emit).toHaveBeenCalledWith('UpdateWidgetData', mrObj); expect(eventHub.$emit).toHaveBeenCalledWith('UpdateWidgetData', mrObj);
expect(window.Flash).toHaveBeenCalledWith( expect(flashSpy).toHaveBeenCalledWith('The merge request can now be merged.', 'notice');
'The merge request can now be merged.',
'notice',
);
done(); done();
}, 333); }, 333);
}); });

View File

@ -0,0 +1,128 @@
# frozen_string_literal: true
require 'fast_spec_helper'
describe Gitlab::Ci::Config::External::Context do
let(:project) { double('Project') }
let(:user) { double('User') }
let(:sha) { '12345' }
let(:attributes) { { project: project, user: user, sha: sha } }
subject(:subject) { described_class.new(**attributes) }
describe 'attributes' do
context 'with values' do
it { is_expected.to have_attributes(**attributes) }
it { expect(subject.expandset).to eq(Set.new) }
it { expect(subject.execution_deadline).to eq(0) }
end
context 'without values' do
let(:attributes) { { project: nil, user: nil, sha: nil } }
it { is_expected.to have_attributes(**attributes) }
it { expect(subject.expandset).to eq(Set.new) }
it { expect(subject.execution_deadline).to eq(0) }
end
end
describe '#set_deadline' do
let(:stubbed_time) { 1 }
before do
allow(subject).to receive(:current_monotonic_time).and_return(stubbed_time)
end
context 'with a float value' do
let(:timeout_seconds) { 10.5.seconds }
it 'updates execution_deadline' do
expect { subject.set_deadline(timeout_seconds) }
.to change { subject.execution_deadline }
.to(timeout_seconds + stubbed_time)
end
end
context 'with nil as a value' do
let(:timeout_seconds) {}
it 'updates execution_deadline' do
expect { subject.set_deadline(timeout_seconds) }
.to change { subject.execution_deadline }
.to(stubbed_time)
end
end
end
describe '#check_execution_time!' do
before do
allow(subject).to receive(:current_monotonic_time).and_return(stubbed_time)
allow(subject).to receive(:execution_deadline).and_return(stubbed_deadline)
end
context 'when execution is expired' do
let(:stubbed_time) { 2 }
let(:stubbed_deadline) { 1 }
it 'raises an error' do
expect { subject.check_execution_time! }
.to raise_error(described_class::TimeoutError)
end
end
context 'when execution is not expired' do
let(:stubbed_time) { 1 }
let(:stubbed_deadline) { 2 }
it 'does not raises any errors' do
expect { subject.check_execution_time! }.not_to raise_error
end
end
context 'without setting a deadline' do
let(:stubbed_time) { 2 }
let(:stubbed_deadline) { 1 }
before do
allow(subject).to receive(:execution_deadline).and_call_original
end
it 'does not raises any errors' do
expect { subject.check_execution_time! }.not_to raise_error
end
end
end
describe '#mutate' do
shared_examples 'a mutated context' do
let(:mutated) { subject.mutate(new_attributes) }
before do
subject.expandset << :a_file
subject.set_deadline(15.seconds)
end
it { expect(mutated).not_to eq(subject) }
it { expect(mutated).to be_a(described_class) }
it { expect(mutated).to have_attributes(new_attributes) }
it { expect(mutated.expandset).to eq(subject.expandset) }
it { expect(mutated.execution_deadline).to eq(mutated.execution_deadline) }
end
context 'with attributes' do
let(:new_attributes) { { project: double, user: double, sha: '56789' } }
it_behaves_like 'a mutated context'
end
context 'without attributes' do
let(:new_attributes) { {} }
it_behaves_like 'a mutated context'
end
end
describe '#sentry_payload' do
it { expect(subject.sentry_payload).to match(a_hash_including(:project, :user)) }
end
end

View File

@ -1,13 +1,14 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'fast_spec_helper' require 'spec_helper'
describe Gitlab::Ci::Config::External::File::Base do describe Gitlab::Ci::Config::External::File::Base do
let(:context) { described_class::Context.new(nil, 'HEAD', nil, Set.new) } let(:context_params) { { sha: 'HEAD' } }
let(:context) { Gitlab::Ci::Config::External::Context.new(**context_params) }
let(:test_class) do let(:test_class) do
Class.new(described_class) do Class.new(described_class) do
def initialize(params, context = {}) def initialize(params, context)
@location = params @location = params
super super
@ -20,6 +21,9 @@ describe Gitlab::Ci::Config::External::File::Base do
before do before do
allow_any_instance_of(test_class) allow_any_instance_of(test_class)
.to receive(:content).and_return('key: value') .to receive(:content).and_return('key: value')
allow_any_instance_of(Gitlab::Ci::Config::External::Context)
.to receive(:check_execution_time!)
end end
describe '#matching?' do describe '#matching?' do

View File

@ -7,10 +7,17 @@ describe Gitlab::Ci::Config::External::File::Local do
set(:user) { create(:user) } set(:user) { create(:user) }
let(:sha) { '12345' } let(:sha) { '12345' }
let(:context) { described_class::Context.new(project, sha, user, Set.new) } let(:context_params) { { project: project, sha: sha, user: user } }
let(:context) { Gitlab::Ci::Config::External::Context.new(**context_params) }
let(:params) { { local: location } } let(:params) { { local: location } }
let(:local_file) { described_class.new(params, context) } let(:local_file) { described_class.new(params, context) }
before do
allow_any_instance_of(Gitlab::Ci::Config::External::Context)
.to receive(:check_execution_time!)
end
describe '#matching?' do describe '#matching?' do
context 'when a local is specified' do context 'when a local is specified' do
let(:params) { { local: 'file' } } let(:params) { { local: 'file' } }
@ -109,7 +116,7 @@ describe Gitlab::Ci::Config::External::File::Local do
describe '#expand_context' do describe '#expand_context' do
let(:location) { 'location.yml' } let(:location) { 'location.yml' }
subject { local_file.send(:expand_context) } subject { local_file.send(:expand_context_attrs) }
it 'inherits project, user and sha' do it 'inherits project, user and sha' do
is_expected.to include(user: user, project: project, sha: sha) is_expected.to include(user: user, project: project, sha: sha)

View File

@ -8,11 +8,15 @@ describe Gitlab::Ci::Config::External::File::Project do
set(:user) { create(:user) } set(:user) { create(:user) }
let(:context_user) { user } let(:context_user) { user }
let(:context) { described_class::Context.new(context_project, '12345', context_user, Set.new) } let(:context_params) { { project: context_project, sha: '12345', user: context_user } }
let(:context) { Gitlab::Ci::Config::External::Context.new(**context_params) }
let(:project_file) { described_class.new(params, context) } let(:project_file) { described_class.new(params, context) }
before do before do
project.add_developer(user) project.add_developer(user)
allow_any_instance_of(Gitlab::Ci::Config::External::Context)
.to receive(:check_execution_time!)
end end
describe '#matching?' do describe '#matching?' do
@ -145,7 +149,7 @@ describe Gitlab::Ci::Config::External::File::Project do
describe '#expand_context' do describe '#expand_context' do
let(:params) { { file: 'file.yml', project: project.full_path, ref: 'master' } } let(:params) { { file: 'file.yml', project: project.full_path, ref: 'master' } }
subject { project_file.send(:expand_context) } subject { project_file.send(:expand_context_attrs) }
it 'inherits user, and target project and sha' do it 'inherits user, and target project and sha' do
is_expected.to include(user: user, project: project, sha: project.commit('master').id) is_expected.to include(user: user, project: project, sha: project.commit('master').id)

View File

@ -5,7 +5,8 @@ require 'spec_helper'
describe Gitlab::Ci::Config::External::File::Remote do describe Gitlab::Ci::Config::External::File::Remote do
include StubRequests include StubRequests
let(:context) { described_class::Context.new(nil, '12345', nil, Set.new) } let(:context_params) { { sha: '12345' } }
let(:context) { Gitlab::Ci::Config::External::Context.new(**context_params) }
let(:params) { { remote: location } } let(:params) { { remote: location } }
let(:remote_file) { described_class.new(params, context) } let(:remote_file) { described_class.new(params, context) }
let(:location) { 'https://gitlab.com/gitlab-org/gitlab-foss/blob/1234/.gitlab-ci-1.yml' } let(:location) { 'https://gitlab.com/gitlab-org/gitlab-foss/blob/1234/.gitlab-ci-1.yml' }
@ -19,6 +20,11 @@ describe Gitlab::Ci::Config::External::File::Remote do
HEREDOC HEREDOC
end end
before do
allow_any_instance_of(Gitlab::Ci::Config::External::Context)
.to receive(:check_execution_time!)
end
describe '#matching?' do describe '#matching?' do
context 'when a remote is specified' do context 'when a remote is specified' do
let(:params) { { remote: 'http://remote' } } let(:params) { { remote: 'http://remote' } }
@ -187,10 +193,10 @@ describe Gitlab::Ci::Config::External::File::Remote do
describe '#expand_context' do describe '#expand_context' do
let(:params) { { remote: 'http://remote' } } let(:params) { { remote: 'http://remote' } }
subject { remote_file.send(:expand_context) } subject { remote_file.send(:expand_context_attrs) }
it 'drops all parameters' do it 'drops all parameters' do
is_expected.to include(user: nil, project: nil, sha: nil) is_expected.to be_empty
end end
end end
end end

View File

@ -6,12 +6,18 @@ describe Gitlab::Ci::Config::External::File::Template do
set(:project) { create(:project) } set(:project) { create(:project) }
set(:user) { create(:user) } set(:user) { create(:user) }
let(:context) { described_class::Context.new(project, '12345', user, Set.new) } let(:context_params) { { project: project, sha: '12345', user: user } }
let(:context) { Gitlab::Ci::Config::External::Context.new(**context_params) }
let(:template) { 'Auto-DevOps.gitlab-ci.yml' } let(:template) { 'Auto-DevOps.gitlab-ci.yml' }
let(:params) { { template: template } } let(:params) { { template: template } }
let(:template_file) { described_class.new(params, context) } let(:template_file) { described_class.new(params, context) }
before do
allow_any_instance_of(Gitlab::Ci::Config::External::Context)
.to receive(:check_execution_time!)
end
describe '#matching?' do describe '#matching?' do
context 'when a template is specified' do context 'when a template is specified' do
let(:params) { { template: 'some-template' } } let(:params) { { template: 'some-template' } }
@ -97,10 +103,10 @@ describe Gitlab::Ci::Config::External::File::Template do
describe '#expand_context' do describe '#expand_context' do
let(:location) { 'location.yml' } let(:location) { 'location.yml' }
subject { template_file.send(:expand_context) } subject { template_file.send(:expand_context_attrs) }
it 'drops all parameters' do it 'drops all parameters' do
is_expected.to include(user: nil, project: nil, sha: nil) is_expected.to be_empty
end end
end end
end end

View File

@ -11,7 +11,8 @@ describe Gitlab::Ci::Config::External::Mapper do
let(:local_file) { '/lib/gitlab/ci/templates/non-existent-file.yml' } let(:local_file) { '/lib/gitlab/ci/templates/non-existent-file.yml' }
let(:remote_url) { 'https://gitlab.com/gitlab-org/gitlab-foss/blob/1234/.gitlab-ci-1.yml' } let(:remote_url) { 'https://gitlab.com/gitlab-org/gitlab-foss/blob/1234/.gitlab-ci-1.yml' }
let(:template_file) { 'Auto-DevOps.gitlab-ci.yml' } let(:template_file) { 'Auto-DevOps.gitlab-ci.yml' }
let(:expandset) { Set.new } let(:context_params) { { project: project, sha: '123456', user: user } }
let(:context) { Gitlab::Ci::Config::External::Context.new(**context_params) }
let(:file_content) do let(:file_content) do
<<~HEREDOC <<~HEREDOC
@ -21,10 +22,13 @@ describe Gitlab::Ci::Config::External::Mapper do
before do before do
stub_full_request(remote_url).to_return(body: file_content) stub_full_request(remote_url).to_return(body: file_content)
allow_any_instance_of(Gitlab::Ci::Config::External::Context)
.to receive(:check_execution_time!)
end end
describe '#process' do describe '#process' do
subject { described_class.new(values, project: project, sha: '123456', user: user, expandset: expandset).process } subject { described_class.new(values, context).process }
context "when single 'include' keyword is defined" do context "when single 'include' keyword is defined" do
context 'when the string is a local file' do context 'when the string is a local file' do

View File

@ -9,12 +9,16 @@ describe Gitlab::Ci::Config::External::Processor do
set(:another_project) { create(:project, :repository) } set(:another_project) { create(:project, :repository) }
set(:user) { create(:user) } set(:user) { create(:user) }
let(:expandset) { Set.new }
let(:sha) { '12345' } let(:sha) { '12345' }
let(:processor) { described_class.new(values, project: project, sha: '12345', user: user, expandset: expandset) } let(:context_params) { { project: project, sha: sha, user: user } }
let(:context) { Gitlab::Ci::Config::External::Context.new(**context_params) }
let(:processor) { described_class.new(values, context) }
before do before do
project.add_developer(user) project.add_developer(user)
allow_any_instance_of(Gitlab::Ci::Config::External::Context)
.to receive(:check_execution_time!)
end end
describe "#perform" do describe "#perform" do

View File

@ -7,6 +7,11 @@ describe Gitlab::Ci::Config do
set(:user) { create(:user) } set(:user) { create(:user) }
before do
allow_any_instance_of(Gitlab::Ci::Config::External::Context)
.to receive(:check_execution_time!)
end
let(:config) do let(:config) do
described_class.new(yml, project: nil, sha: nil, user: nil) described_class.new(yml, project: nil, sha: nil, user: nil)
end end
@ -303,6 +308,49 @@ describe Gitlab::Ci::Config do
end end
end end
context "when it takes too long to evaluate includes" do
before do
allow_any_instance_of(Gitlab::Ci::Config::External::Context)
.to receive(:check_execution_time!)
.and_call_original
allow_any_instance_of(Gitlab::Ci::Config::External::Context)
.to receive(:set_deadline)
.with(described_class::TIMEOUT_SECONDS)
.and_call_original
allow_any_instance_of(Gitlab::Ci::Config::External::Context)
.to receive(:execution_expired?)
.and_return(true)
end
it 'raises error TimeoutError' do
expect(Gitlab::Sentry).to receive(:track_exception)
expect { config }.to raise_error(
described_class::ConfigError,
'Resolving config took longer than expected'
)
end
end
context 'when context expansion timeout is disabled' do
before do
allow_any_instance_of(Gitlab::Ci::Config::External::Context)
.to receive(:check_execution_time!)
.and_call_original
allow(Feature)
.to receive(:enabled?)
.with(:ci_limit_yaml_expansion, project, default_enabled: true)
.and_return(false)
end
it 'does not raises errors' do
expect { config }.not_to raise_error
end
end
describe 'external file version' do describe 'external file version' do
context 'when external local file SHA is defined' do context 'when external local file SHA is defined' do
it 'is using a defined value' do it 'is using a defined value' do

View File

@ -403,12 +403,30 @@ describe API::Internal::Base do
end end
context 'when receive_max_input_size has been updated' do context 'when receive_max_input_size has been updated' do
it 'returns custom git config' do before do
allow(Gitlab::CurrentSettings).to receive(:receive_max_input_size) { 1 } allow(Gitlab::CurrentSettings).to receive(:receive_max_input_size) { 1 }
end
it 'returns custom git config' do
push(key, project) push(key, project)
expect(json_response["git_config_options"]).to be_present expect(json_response["git_config_options"]).to be_present
expect(json_response["git_config_options"]).to include("uploadpack.allowFilter=true")
expect(json_response["git_config_options"]).to include("uploadpack.allowAnySHA1InWant=true")
end
context 'when gitaly_upload_pack_filter feature flag is disabled' do
before do
stub_feature_flags(gitaly_upload_pack_filter: { enabled: false, thing: project })
end
it 'does not include allowFilter and allowAnySha1InWant in the git config options' do
push(key, project)
expect(json_response["git_config_options"]).to be_present
expect(json_response["git_config_options"]).not_to include("uploadpack.allowFilter=true")
expect(json_response["git_config_options"]).not_to include("uploadpack.allowAnySHA1InWant=true")
end
end end
end end