Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-06-02 09:08:01 +00:00
parent f4251f2694
commit 8c826685ec
46 changed files with 1073 additions and 713 deletions

1
.gitignore vendored
View File

@ -92,3 +92,4 @@ webpack-dev-server.json
/.nvimrc /.nvimrc
.solargraph.yml .solargraph.yml
apollo.config.js apollo.config.js
/tmp/matching_foss_tests.txt

View File

@ -324,3 +324,25 @@ db:rollback geo:
- bundle exec rake geo:db:migrate - bundle exec rake geo:db:migrate
# EE: default refs (MRs, master, schedules) jobs # # EE: default refs (MRs, master, schedules) jobs #
################################################## ##################################################
##################################################
# EE: Canonical MR pipelines
rspec foss-impact:
extends:
- .rspec-base
- .as-if-foss
- .rails:rules:ee-mr-only
- .use-pg11
script:
- install_gitlab_gem
- run_timed_command "scripts/gitaly-test-build"
- run_timed_command "scripts/gitaly-test-spawn"
- source scripts/rspec_helpers.sh
- tooling/bin/find_foss_tests tmp/matching_foss_tests.txt
- rspec_simple_job "--tag ~quarantine --tag ~geo --tag ~level:migration $(cat tmp/matching_foss_tests.txt)"
artifacts:
expire_in: 7d
paths:
- tmp/matching_foss_tests.txt
# EE: Merge Request pipelines
##################################################

View File

@ -446,6 +446,15 @@
- <<: *if-master-refs - <<: *if-master-refs
changes: *code-backstage-patterns changes: *code-backstage-patterns
.rails:rules:ee-mr-only:
rules:
- <<: *if-not-ee
when: never
- <<: *if-security-merge-request
changes: *code-backstage-patterns
- <<: *if-dot-com-gitlab-org-merge-request
changes: *code-backstage-patterns
.rails:rules:downtime_check: .rails:rules:downtime_check:
rules: rules:
- <<: *if-merge-request - <<: *if-merge-request

View File

@ -1 +1 @@
8.33.0 8.34.0

View File

@ -41,7 +41,7 @@ export default {
variant="success" variant="success"
@click="openFileUpload" @click="openFileUpload"
> >
{{ s__('DesignManagement|Add designs') }} {{ s__('DesignManagement|Upload designs') }}
<gl-loading-icon v-if="isSaving" inline class="ml-1" /> <gl-loading-icon v-if="isSaving" inline class="ml-1" />
</gl-deprecated-button> </gl-deprecated-button>

View File

@ -1,10 +1,8 @@
/* eslint-disable no-return-assign, consistent-return, class-methods-use-this */ /* eslint-disable no-return-assign, consistent-return, class-methods-use-this */
import $ from 'jquery'; import $ from 'jquery';
import { escape, throttle } from 'lodash'; import { throttle } from 'lodash';
import { s__, __, sprintf } from '~/locale'; import { s__, __, sprintf } from '~/locale';
import { getIdenticonBackgroundClass, getIdenticonTitle } from '~/helpers/avatar_helper';
import axios from './lib/utils/axios_utils';
import { import {
isInGroupsPage, isInGroupsPage,
isInProjectPage, isInProjectPage,
@ -67,15 +65,11 @@ function setSearchOptions() {
} }
} }
export class SearchAutocomplete { export class GlobalSearchInput {
constructor({ wrap, optsEl, autocompletePath, projectId, projectRef } = {}) { constructor({ wrap } = {}) {
setSearchOptions(); setSearchOptions();
this.bindEventContext(); this.bindEventContext();
this.wrap = wrap || $('.search'); this.wrap = wrap || $('.search');
this.optsEl = optsEl || this.wrap.find('.search-autocomplete-opts');
this.autocompletePath = autocompletePath || this.optsEl.data('autocompletePath');
this.projectId = projectId || (this.optsEl.data('autocompleteProjectId') || '');
this.projectRef = projectRef || (this.optsEl.data('autocompleteProjectRef') || '');
this.dropdown = this.wrap.find('.dropdown'); this.dropdown = this.wrap.find('.dropdown');
this.dropdownToggle = this.wrap.find('.js-dropdown-search-toggle'); this.dropdownToggle = this.wrap.find('.js-dropdown-search-toggle');
this.dropdownMenu = this.dropdown.find('.dropdown-menu'); this.dropdownMenu = this.dropdown.find('.dropdown-menu');
@ -92,7 +86,7 @@ export class SearchAutocomplete {
// Only when user is logged in // Only when user is logged in
if (gon.current_user_id) { if (gon.current_user_id) {
this.createAutocomplete(); this.createGlobalSearchInput();
} }
this.bindEvents(); this.bindEvents();
@ -117,7 +111,7 @@ export class SearchAutocomplete {
return (this.originalState = this.serializeState()); return (this.originalState = this.serializeState());
} }
createAutocomplete() { createGlobalSearchInput() {
return this.searchInput.glDropdown({ return this.searchInput.glDropdown({
filterInputBlur: false, filterInputBlur: false,
filterable: true, filterable: true,
@ -149,116 +143,17 @@ export class SearchAutocomplete {
if (glDropdownInstance) { if (glDropdownInstance) {
glDropdownInstance.filter.options.callback(contents); glDropdownInstance.filter.options.callback(contents);
} }
this.enableAutocomplete(); this.enableDropdown();
} }
return; return;
} }
// Prevent multiple ajax calls const options = this.scopedSearchOptions(term);
if (this.loadingSuggestions) {
return;
}
this.loadingSuggestions = true; callback(options);
return axios this.highlightFirstRow();
.get(this.autocompletePath, { this.setScrollFade();
params: {
project_id: this.projectId,
project_ref: this.projectRef,
term,
},
})
.then(response => {
const options = this.scopedSearchOptions(term);
// List results
let lastCategory = null;
for (let i = 0, len = response.data.length; i < len; i += 1) {
const suggestion = response.data[i];
// Add group header before list each group
if (lastCategory !== suggestion.category) {
options.push({ type: 'separator' });
options.push({
type: 'header',
content: suggestion.category,
});
lastCategory = suggestion.category;
}
// Add the suggestion
options.push({
id: `${suggestion.category.toLowerCase()}-${suggestion.id}`,
icon: this.getAvatar(suggestion),
category: suggestion.category,
text: suggestion.label,
url: suggestion.url,
});
}
callback(options);
this.loadingSuggestions = false;
this.highlightFirstRow();
this.setScrollFade();
})
.catch(() => {
this.loadingSuggestions = false;
});
}
getCategoryContents() {
const userName = gon.current_username;
const { projectOptions, groupOptions, dashboardOptions } = gl;
// Get options
let options;
if (isInProjectPage() && projectOptions) {
options = projectOptions[getProjectSlug()];
} else if (isInGroupsPage() && groupOptions) {
options = groupOptions[getGroupSlug()];
} else if (dashboardOptions) {
options = dashboardOptions;
}
const { issuesPath, mrPath, name, issuesDisabled } = options;
const baseItems = [];
if (name) {
baseItems.push({
type: 'header',
content: `${name}`,
});
}
const issueItems = [
{
text: s__('SearchAutocomplete|Issues assigned to me'),
url: `${issuesPath}/?assignee_username=${userName}`,
},
{
text: s__("SearchAutocomplete|Issues I've created"),
url: `${issuesPath}/?author_username=${userName}`,
},
];
const mergeRequestItems = [
{
text: s__('SearchAutocomplete|Merge requests assigned to me'),
url: `${mrPath}/?assignee_username=${userName}`,
},
{
text: s__("SearchAutocomplete|Merge requests I've created"),
url: `${mrPath}/?author_username=${userName}`,
},
];
let items;
if (issuesDisabled) {
items = baseItems.concat(mergeRequestItems);
} else {
items = baseItems.concat(...issueItems, ...mergeRequestItems);
}
return items;
} }
// Add option to proceed with the search for each // Add option to proceed with the search for each
@ -343,7 +238,7 @@ export class SearchAutocomplete {
}); });
} }
enableAutocomplete() { enableDropdown() {
this.setScrollFade(); this.setScrollFade();
// No need to enable anything if user is not logged in // No need to enable anything if user is not logged in
@ -360,7 +255,7 @@ export class SearchAutocomplete {
} }
onSearchInputChange() { onSearchInputChange() {
this.enableAutocomplete(); this.enableDropdown();
} }
onSearchInputKeyUp(e) { onSearchInputKeyUp(e) {
@ -369,7 +264,7 @@ export class SearchAutocomplete {
this.restoreOriginalState(); this.restoreOriginalState();
break; break;
case KEYCODE.ENTER: case KEYCODE.ENTER:
this.disableAutocomplete(); this.disableDropdown();
break; break;
default: default:
} }
@ -422,7 +317,7 @@ export class SearchAutocomplete {
return results; return results;
} }
disableAutocomplete() { disableDropdown() {
if (!this.searchInput.hasClass('js-autocomplete-disabled') && this.dropdown.hasClass('show')) { if (!this.searchInput.hasClass('js-autocomplete-disabled') && this.dropdown.hasClass('show')) {
this.searchInput.addClass('js-autocomplete-disabled'); this.searchInput.addClass('js-autocomplete-disabled');
this.dropdownToggle.dropdown('toggle'); this.dropdownToggle.dropdown('toggle');
@ -438,16 +333,8 @@ export class SearchAutocomplete {
onClick(item, $el, e) { onClick(item, $el, e) {
if (window.location.pathname.indexOf(item.url) !== -1) { if (window.location.pathname.indexOf(item.url) !== -1) {
if (!e.metaKey) e.preventDefault(); if (!e.metaKey) e.preventDefault();
/* eslint-disable-next-line @gitlab/require-i18n-strings */
if (item.category === 'Projects') {
this.projectInputEl.val(item.id);
}
// eslint-disable-next-line @gitlab/require-i18n-strings
if (item.category === 'Groups') {
this.groupInputEl.val(item.id);
}
$el.removeClass('is-active'); $el.removeClass('is-active');
this.disableAutocomplete(); this.disableDropdown();
return this.searchInput.val('').focus(); return this.searchInput.val('').focus();
} }
} }
@ -456,20 +343,58 @@ export class SearchAutocomplete {
this.searchInput.data('glDropdown').highlightRowAtIndex(null, 0); this.searchInput.data('glDropdown').highlightRowAtIndex(null, 0);
} }
getAvatar(item) { getCategoryContents() {
if (!Object.hasOwnProperty.call(item, 'avatar_url')) { const userName = gon.current_username;
return false; const { projectOptions, groupOptions, dashboardOptions } = gl;
// Get options
let options;
if (isInProjectPage() && projectOptions) {
options = projectOptions[getProjectSlug()];
} else if (isInGroupsPage() && groupOptions) {
options = groupOptions[getGroupSlug()];
} else if (dashboardOptions) {
options = dashboardOptions;
} }
const { label, id } = item; const { issuesPath, mrPath, name, issuesDisabled } = options;
const avatarUrl = item.avatar_url; const baseItems = [];
const avatar = avatarUrl
? `<img class="search-item-avatar" src="${avatarUrl}" />`
: `<div class="s16 avatar identicon ${getIdenticonBackgroundClass(id)}">${getIdenticonTitle(
escape(label),
)}</div>`;
return avatar; if (name) {
baseItems.push({
type: 'header',
content: `${name}`,
});
}
const issueItems = [
{
text: s__('SearchAutocomplete|Issues assigned to me'),
url: `${issuesPath}/?assignee_username=${userName}`,
},
{
text: s__("SearchAutocomplete|Issues I've created"),
url: `${issuesPath}/?author_username=${userName}`,
},
];
const mergeRequestItems = [
{
text: s__('SearchAutocomplete|Merge requests assigned to me'),
url: `${mrPath}/?assignee_username=${userName}`,
},
{
text: s__("SearchAutocomplete|Merge requests I've created"),
url: `${mrPath}/?author_username=${userName}`,
},
];
let items;
if (issuesDisabled) {
items = baseItems.concat(mergeRequestItems);
} else {
items = baseItems.concat(...issueItems, ...mergeRequestItems);
}
return items;
} }
isScrolledUp() { isScrolledUp() {
@ -495,6 +420,6 @@ export class SearchAutocomplete {
} }
} }
export default function initSearchAutocomplete(opts) { export default function initGlobalSearchInput(opts) {
return new SearchAutocomplete(opts); return new GlobalSearchInput(opts);
} }

View File

@ -32,7 +32,7 @@ import initFrequentItemDropdowns from './frequent_items';
import initBreadcrumbs from './breadcrumb'; import initBreadcrumbs from './breadcrumb';
import initUsagePingConsent from './usage_ping_consent'; import initUsagePingConsent from './usage_ping_consent';
import initPerformanceBar from './performance_bar'; import initPerformanceBar from './performance_bar';
import initSearchAutocomplete from './search_autocomplete'; import initGlobalSearchInput from './global_search_input';
import GlFieldErrors from './gl_field_errors'; import GlFieldErrors from './gl_field_errors';
import initUserPopovers from './user_popovers'; import initUserPopovers from './user_popovers';
import initBroadcastNotifications from './broadcast_notification'; import initBroadcastNotifications from './broadcast_notification';
@ -110,7 +110,7 @@ function deferredInitialisation() {
initFrequentItemDropdowns(); initFrequentItemDropdowns();
initPersistentUserCallouts(); initPersistentUserCallouts();
if (document.querySelector('.search')) initSearchAutocomplete(); if (document.querySelector('.search')) initGlobalSearchInput();
addSelectOnFocusBehaviour('.js-select-on-focus'); addSelectOnFocusBehaviour('.js-select-on-focus');

View File

@ -51,21 +51,6 @@ class SearchController < ApplicationController
render json: { count: count } render json: { count: count }
end end
# rubocop: disable CodeReuse/ActiveRecord
def autocomplete
term = params[:term]
if params[:project_id].present?
@project = Project.find_by(id: params[:project_id])
@project = nil unless can?(current_user, :read_project, @project)
end
@ref = params[:project_ref] if params[:project_ref].present?
render json: search_autocomplete_opts(term).to_json
end
# rubocop: enable CodeReuse/ActiveRecord
private private
def preload_method def preload_method

View File

@ -3,28 +3,6 @@
module SearchHelper module SearchHelper
SEARCH_PERMITTED_PARAMS = [:search, :scope, :project_id, :group_id, :repository_ref, :snippets].freeze SEARCH_PERMITTED_PARAMS = [:search, :scope, :project_id, :group_id, :repository_ref, :snippets].freeze
def search_autocomplete_opts(term)
return unless current_user
resources_results = [
groups_autocomplete(term),
projects_autocomplete(term)
].flatten
search_pattern = Regexp.new(Regexp.escape(term), "i")
generic_results = project_autocomplete + default_autocomplete + help_autocomplete
generic_results.concat(default_autocomplete_admin) if current_user.admin?
generic_results.select! { |result| result[:label] =~ search_pattern }
[
resources_results,
generic_results
].flatten.uniq do |item|
item[:label]
end
end
def search_entries_info(collection, scope, term) def search_entries_info(collection, scope, term)
return if collection.to_a.empty? return if collection.to_a.empty?
@ -95,91 +73,6 @@ module SearchHelper
private private
# Autocomplete results for various settings pages
def default_autocomplete
[
{ category: "Settings", label: _("User settings"), url: profile_path },
{ category: "Settings", label: _("SSH Keys"), url: profile_keys_path },
{ category: "Settings", label: _("Dashboard"), url: root_path }
]
end
# Autocomplete results for settings pages, for admins
def default_autocomplete_admin
[
{ category: "Settings", label: _("Admin Section"), url: admin_root_path }
]
end
# Autocomplete results for internal help pages
def help_autocomplete
[
{ category: "Help", label: _("API Help"), url: help_page_path("api/README") },
{ category: "Help", label: _("Markdown Help"), url: help_page_path("user/markdown") },
{ category: "Help", label: _("Permissions Help"), url: help_page_path("user/permissions") },
{ category: "Help", label: _("Public Access Help"), url: help_page_path("public_access/public_access") },
{ category: "Help", label: _("Rake Tasks Help"), url: help_page_path("raketasks/README") },
{ category: "Help", label: _("SSH Keys Help"), url: help_page_path("ssh/README") },
{ category: "Help", label: _("System Hooks Help"), url: help_page_path("system_hooks/system_hooks") },
{ category: "Help", label: _("Webhooks Help"), url: help_page_path("user/project/integrations/webhooks") },
{ category: "Help", label: _("Workflow Help"), url: help_page_path("workflow/README") }
]
end
# Autocomplete results for the current project, if it's defined
def project_autocomplete
if @project && @project.repository.root_ref
ref = @ref || @project.repository.root_ref
[
{ category: "In this project", label: _("Files"), url: project_tree_path(@project, ref) },
{ category: "In this project", label: _("Commits"), url: project_commits_path(@project, ref) },
{ category: "In this project", label: _("Network"), url: project_network_path(@project, ref) },
{ category: "In this project", label: _("Graph"), url: project_graph_path(@project, ref) },
{ category: "In this project", label: _("Issues"), url: project_issues_path(@project) },
{ category: "In this project", label: _("Merge Requests"), url: project_merge_requests_path(@project) },
{ category: "In this project", label: _("Milestones"), url: project_milestones_path(@project) },
{ category: "In this project", label: _("Snippets"), url: project_snippets_path(@project) },
{ category: "In this project", label: _("Members"), url: project_project_members_path(@project) },
{ category: "In this project", label: _("Wiki"), url: project_wikis_path(@project) }
]
else
[]
end
end
# Autocomplete results for the current user's groups
# rubocop: disable CodeReuse/ActiveRecord
def groups_autocomplete(term, limit = 5)
current_user.authorized_groups.order_id_desc.search(term).limit(limit).map do |group|
{
category: "Groups",
id: group.id,
label: "#{search_result_sanitize(group.full_name)}",
url: group_path(group),
avatar_url: group.avatar_url || ''
}
end
end
# rubocop: enable CodeReuse/ActiveRecord
# Autocomplete results for the current user's projects
# rubocop: disable CodeReuse/ActiveRecord
def projects_autocomplete(term, limit = 5)
current_user.authorized_projects.order_id_desc.search_by_title(term)
.sorted_by_stars_desc.non_archived.limit(limit).map do |p|
{
category: "Projects",
id: p.id,
value: "#{search_result_sanitize(p.name)}",
label: "#{search_result_sanitize(p.full_name)}",
url: project_path(p),
avatar_url: p.avatar_url || ''
}
end
end
# rubocop: enable CodeReuse/ActiveRecord
def search_result_sanitize(str) def search_result_sanitize(str)
Sanitize.clean(str) Sanitize.clean(str)
end end

View File

@ -149,7 +149,9 @@ class Event < ApplicationRecord
def visible_to_user?(user = nil) def visible_to_user?(user = nil)
return false unless capability.present? return false unless capability.present?
Ability.allowed?(user, capability, permission_object) capability.all? do |rule|
Ability.allowed?(user, rule, permission_object)
end
end end
def resource_parent def resource_parent
@ -361,34 +363,30 @@ class Event < ApplicationRecord
protected protected
# rubocop:disable Metrics/CyclomaticComplexity
# rubocop:disable Metrics/PerceivedComplexity
#
# TODO Refactor this method so we no longer need to disable the above cops
# https://gitlab.com/gitlab-org/gitlab/-/issues/216879.
def capability def capability
@capability ||= begin @capability ||= begin
if push_action? || commit_note? capabilities.flat_map do |ability, syms|
:download_code if syms.any? { |sym| send(sym) } # rubocop: disable GitlabSecurity/PublicSend
elsif membership_changed? || created_project_action? [ability]
:read_project else
elsif issue? || issue_note? []
:read_issue end
elsif merge_request? || merge_request_note? end
:read_merge_request end
elsif personal_snippet_note? || project_snippet_note? end
:read_snippet
elsif milestone? def capabilities
:read_milestone {
elsif wiki_page? download_code: %i[push_action? commit_note?],
:read_wiki read_project: %i[membership_changed? created_project_action?],
elsif design_note? || design? read_issue: %i[issue? issue_note?],
:read_design read_merge_request: %i[merge_request? merge_request_note?],
end read_snippet: %i[personal_snippet_note? project_snippet_note?],
end read_milestone: %i[milestone?],
read_wiki: %i[wiki_page?],
read_design: %i[design_note? design?]
}
end end
# rubocop:enable Metrics/CyclomaticComplexity
# rubocop:enable Metrics/PerceivedComplexity
private private

View File

@ -6,19 +6,18 @@ module AutoMerge
include MergeRequests::AssignsMergeParams include MergeRequests::AssignsMergeParams
def execute(merge_request) def execute(merge_request)
assign_allowed_merge_params(merge_request, params.merge(auto_merge_strategy: strategy)) ActiveRecord::Base.transaction do
register_auto_merge_parameters!(merge_request)
merge_request.auto_merge_enabled = true yield if block_given?
merge_request.merge_user = current_user end
return :failed unless merge_request.save
yield if block_given?
# Notify the event that auto merge is enabled or merge param is updated # Notify the event that auto merge is enabled or merge param is updated
AutoMergeProcessWorker.perform_async(merge_request.id) AutoMergeProcessWorker.perform_async(merge_request.id)
strategy.to_sym strategy.to_sym
rescue => e
track_exception(e, merge_request)
:failed
end end
def update(merge_request) def update(merge_request)
@ -30,23 +29,27 @@ module AutoMerge
end end
def cancel(merge_request) def cancel(merge_request)
if clear_auto_merge_parameters(merge_request) ActiveRecord::Base.transaction do
clear_auto_merge_parameters!(merge_request)
yield if block_given? yield if block_given?
success
else
error("Can't cancel the automatic merge", 406)
end end
success
rescue => e
track_exception(e, merge_request)
error("Can't cancel the automatic merge", 406)
end end
def abort(merge_request, reason) def abort(merge_request, reason)
if clear_auto_merge_parameters(merge_request) ActiveRecord::Base.transaction do
clear_auto_merge_parameters!(merge_request)
yield if block_given? yield if block_given?
success
else
error("Can't abort the automatic merge", 406)
end end
success
rescue => e
track_exception(e, merge_request)
error("Can't abort the automatic merge", 406)
end end
def available_for?(merge_request) def available_for?(merge_request)
@ -65,7 +68,14 @@ module AutoMerge
end end
end end
def clear_auto_merge_parameters(merge_request) def register_auto_merge_parameters!(merge_request)
assign_allowed_merge_params(merge_request, params.merge(auto_merge_strategy: strategy))
merge_request.auto_merge_enabled = true
merge_request.merge_user = current_user
merge_request.save!
end
def clear_auto_merge_parameters!(merge_request)
merge_request.auto_merge_enabled = false merge_request.auto_merge_enabled = false
merge_request.merge_user = nil merge_request.merge_user = nil
@ -76,7 +86,11 @@ module AutoMerge
'auto_merge_strategy' 'auto_merge_strategy'
) )
merge_request.save merge_request.save!
end
def track_exception(error, merge_request)
Gitlab::ErrorTracking.track_exception(error, merge_request_id: merge_request&.id)
end end
end end
end end

View File

@ -150,7 +150,7 @@ module Projects
if @project.save if @project.save
unless @project.gitlab_project_import? unless @project.gitlab_project_import?
create_services_from_active_templates(@project) create_services_from_active_instances_or_templates(@project)
@project.create_labels @project.create_labels
end end
@ -175,15 +175,6 @@ module Projects
@project @project
end end
# rubocop: disable CodeReuse/ActiveRecord
def create_services_from_active_templates(project)
Service.where(template: true, active: true).each do |template|
service = Service.build_from_integration(project.id, template)
service.save!
end
end
# rubocop: enable CodeReuse/ActiveRecord
def create_prometheus_service def create_prometheus_service
service = @project.find_or_initialize_service(::PrometheusService.to_param) service = @project.find_or_initialize_service(::PrometheusService.to_param)
@ -225,6 +216,15 @@ module Projects
private private
# rubocop: disable CodeReuse/ActiveRecord
def create_services_from_active_instances_or_templates(project)
Service.active.where(instance: true).or(Service.active.where(template: true)).group_by(&:type).each do |type, records|
service = records.find(&:instance?) || records.find(&:template?)
Service.build_from_integration(project.id, service).save!
end
end
# rubocop: enable CodeReuse/ActiveRecord
def project_namespace def project_namespace
@project_namespace ||= Namespace.find_by_id(@params[:namespace_id]) || current_user.namespace @project_namespace ||= Namespace.find_by_id(@params[:namespace_id]) || current_user.namespace
end end

View File

@ -2,7 +2,7 @@
= form_tag search_path, method: :get, class: 'form-inline' do |f| = form_tag search_path, method: :get, class: 'form-inline' do |f|
.search-input-container .search-input-container
.search-input-wrap .search-input-wrap
.dropdown{ data: { url: search_autocomplete_path } } .dropdown
= search_field_tag 'search', nil, placeholder: _('Search or jump to…'), = search_field_tag 'search', nil, placeholder: _('Search or jump to…'),
class: 'search-input dropdown-menu-toggle no-outline js-search-dashboard-options', class: 'search-input dropdown-menu-toggle no-outline js-search-dashboard-options',
spellcheck: false, spellcheck: false,
@ -37,6 +37,3 @@
-# workaround for non-JS feature specs, see spec/support/helpers/search_helpers.rb -# workaround for non-JS feature specs, see spec/support/helpers/search_helpers.rb
- if ENV['RAILS_ENV'] == 'test' - if ENV['RAILS_ENV'] == 'test'
%noscript= button_tag 'Search' %noscript= button_tag 'Search'
.search-autocomplete-opts.hide{ :'data-autocomplete-path' => search_autocomplete_path,
:'data-autocomplete-project-id' => search_context.project.try(:id),
:'data-autocomplete-project-ref' => search_context.ref }

View File

@ -0,0 +1,5 @@
---
title: Rename Add Designs button
merge_request: 33491
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Remove all search autocomplete for groups/projects/other
merge_request: 31187
author:
type: removed

View File

@ -0,0 +1,5 @@
---
title: Wrap auto merge parameters update in database transaction
merge_request: 33471
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Update GitLab Workhorse to v8.34.0
merge_request: 33543
author:
type: fixed

View File

@ -43,6 +43,7 @@
- digital_experience_management - digital_experience_management
- disaster_recovery - disaster_recovery
- dynamic_application_security_testing - dynamic_application_security_testing
- editor_extension
- epics - epics
- error_tracking - error_tracking
- feature_flags - feature_flags
@ -52,6 +53,7 @@
- geo_replication - geo_replication
- git_lfs - git_lfs
- gitaly - gitaly
- gitlab_docs
- gitlab_handbook - gitlab_handbook
- gitter - gitter
- global_search - global_search
@ -82,6 +84,7 @@
- pages - pages
- pki_management - pki_management
- planning_analytics - planning_analytics
- product_analytics
- quality_management - quality_management
- release_evidence - release_evidence
- release_orchestration - release_orchestration
@ -100,7 +103,6 @@
- source_code_management - source_code_management
- static_application_security_testing - static_application_security_testing
- static_site_editor - static_site_editor
- status_page
- subgroups - subgroups
- templates - templates
- time_tracking - time_tracking

View File

@ -148,6 +148,7 @@ if Gitlab::Metrics.enabled? && !Rails.env.test? && !(Rails.env.development? && d
config.middleware.use(Gitlab::Metrics::RackMiddleware) config.middleware.use(Gitlab::Metrics::RackMiddleware)
config.middleware.use(Gitlab::Middleware::RailsQueueDuration) config.middleware.use(Gitlab::Middleware::RailsQueueDuration)
config.middleware.use(Gitlab::Metrics::RedisRackMiddleware) config.middleware.use(Gitlab::Metrics::RedisRackMiddleware)
config.middleware.use(Gitlab::Metrics::ElasticsearchRackMiddleware)
end end
Sidekiq.configure_server do |config| Sidekiq.configure_server do |config|

View File

@ -58,7 +58,6 @@ Rails.application.routes.draw do
# Search # Search
get 'search' => 'search#show' get 'search' => 'search#show'
get 'search/autocomplete' => 'search#autocomplete', as: :search_autocomplete
get 'search/count' => 'search#count', as: :search_count get 'search/count' => 'search#count', as: :search_count
# JSON Web Token # JSON Web Token

View File

@ -94,6 +94,8 @@ The following metrics are available:
| `http_request_duration_seconds` | Histogram | 9.4 | HTTP response time from rack middleware | `method`, `status` | | `http_request_duration_seconds` | Histogram | 9.4 | HTTP response time from rack middleware | `method`, `status` |
| `http_redis_requests_duration_seconds` | Histogram | 13.1 | Redis requests duration during web transactions | `controller`, `action` | | `http_redis_requests_duration_seconds` | Histogram | 13.1 | Redis requests duration during web transactions | `controller`, `action` |
| `http_redis_requests_total` | Counter | 13.1 | Redis requests count during web transactions | `controller`, `action` | | `http_redis_requests_total` | Counter | 13.1 | Redis requests count during web transactions | `controller`, `action` |
| `http_elasticsearch_requests_duration_seconds` **(STARTER)** | Histogram | 13.1 | Elasticsearch requests duration during web transactions | `controller`, `action` |
| `http_elasticsearch_requests_total` **(STARTER)** | Counter | 13.1 | Elasticsearch requests count during web transactions | `controller`, `action` |
| `pipelines_created_total` | Counter | 9.4 | Counter of pipelines created | | | `pipelines_created_total` | Counter | 9.4 | Counter of pipelines created | |
| `rack_uncaught_errors_total` | Counter | 9.4 | Rack connections handling uncaught errors count | | | `rack_uncaught_errors_total` | Counter | 9.4 | Rack connections handling uncaught errors count | |
| `user_session_logins_total` | Counter | 9.4 | Counter of how many users have logged in | | | `user_session_logins_total` | Counter | 9.4 | Counter of how many users have logged in | |

View File

@ -60,8 +60,10 @@ the following documents:
- [GitLab CI/CD basic workflow](introduction/index.md#basic-cicd-workflow). - [GitLab CI/CD basic workflow](introduction/index.md#basic-cicd-workflow).
- [Step-by-step guide for writing `.gitlab-ci.yml` for the first time](../user/project/pages/getting_started_part_four.md). - [Step-by-step guide for writing `.gitlab-ci.yml` for the first time](../user/project/pages/getting_started_part_four.md).
If you're coming over from Jenkins, you can also check out our handy [reference](jenkins/index.md) If you're migrating from another CI/CD tool, check out our handy references:
for converting your pipelines.
- [Migrating from CircleCI](migration/circleci.md)
- [Migrating from Jenkins](jenkins/index.md)
You can also get started by using one of the You can also get started by using one of the
[`.gitlab-ci.yml` templates](https://gitlab.com/gitlab-org/gitlab-foss/tree/master/lib/gitlab/ci/templates) [`.gitlab-ci.yml` templates](https://gitlab.com/gitlab-org/gitlab-foss/tree/master/lib/gitlab/ci/templates)

View File

@ -0,0 +1,332 @@
---
comments: false
type: index, howto
---
# Migrating from CircleCI
If you are currently using CircleCI, you can migrate your CI/CD pipelines to [GitLab CI/CD](../introduction/index.md),
and start making use of all its powerful features. Check out our
[CircleCI vs GitLab](https://about.gitlab.com/devops-tools/circle-ci-vs-gitlab.html)
comparison to see what's different.
We have collected several resources that you may find useful before starting to migrate.
The [Quick Start Guide](../quick_start/README.md) is a good overview of how GitLab CI/CD works. You may also be interested in [Auto DevOps](../../topics/autodevops/index.md) which can be used to build, test, and deploy your applications with little to no configuration needed at all.
For advanced CI/CD teams, [custom project templates](../../user/admin_area/custom_project_templates.md) can enable the reuse of pipeline configurations.
If you have questions that are not answered here, the [GitLab community forum](https://forum.gitlab.com/) can be a great resource.
## `config.yml` vs `gitlab-ci.yml`
CircleCI's `config.yml` configuration file defines scripts, jobs, and workflows (known as "stages" in GitLab). In GitLab, a similar approach is used with a `.gitlab-ci.yml` file in the root directory of your repository.
### Jobs
In CircleCI, jobs are a collection of steps to perform a specific task. In GitLab, [jobs](../yaml/README.md#introduction) are also a fundamental element in the configuration file. The `checkout` parameter is not necessary in GitLab CI/CD as the repository is automatically fetched.
CircleCI example job definition:
```yaml
jobs:
job1:
steps:
- checkout
- run: "execute-script-for-job1"
```
Example of the same job definition in GitLab CI/CD:
``` yaml
job1:
script: "execute-script-for-job1"
```
### Docker image definition
CircleCI defines images at the job level, which is also supported by GitLab CI/CD. Additionally, GitLab CI/CD supports setting this globally to be used by all jobs that don't have `image` defined.
CircleCI example image definition:
```yaml
jobs:
job1:
docker:
- image: ruby:2.6
```
Example of the same image definition in GitLab CI/CD:
```yaml
job1:
image: ruby:2.6
```
### Workflows
CircleCI determines the run order for jobs with `workflows`. This is also used to determine concurrent, sequential, scheduled, or manual runs. The equivalent function in GitLab CI/CD is called [stages](../yaml/README.md#stages). Jobs on the same stage run in parallel, and only run after previous stages complete. Execution of the next stage is skipped when a job fails by default, but this can be allowed to continue even [after a failed job](../yaml/README.md#allow_failure).
See [the Pipeline Architecture Overview](../pipelines/pipeline_architectures.md) for guidance on different types of pipelines that you can use. Pipelines can be tailored to meet your needs, such as for a large complex project or a monorepo with independent defined components.
#### Parallel and sequential job execution
The following examples show how jobs can run in parallel, or sequentially:
1. `job1` and `job2` run in parallel (in the `build` stage for GitLab CI/CD).
1. `job3` runs only after `job1` and `job2` complete successfully (in the `test` stage).
1. `job4` runs only after `job3` completes successfully (in the `deploy` stage).
CircleCI example with `workflows`:
```yaml
version: 2
jobs:
job1:
steps:
- checkout
- run: make build dependencies
job2:
steps:
- run: make build artifacts
job3:
steps:
- run: make test
job4:
steps:
- run: make deploy
workflows:
version: 2
jobs:
- job1
- job2
- job3:
requires:
- job1
- job2
- job4:
requires:
- job3
```
Example of the same workflow as `stages` in GitLab CI/CD:
```yaml
stages:
- build
- test
- deploy
job 1:
stage: build
script: make build dependencies
job 2:
stage: build
script: make build artifacts
job3:
stage: test
script: make test
job4:
stage: deploy
script: make deploy
```
#### Scheduled run
GitLab CI/CD has an easy to use UI to [schedule pipelines](../pipelines/schedules.md). Also, [rules](../yaml/README.md#rules) can be used to determine if jobs should be included or excluded from a scheduled pipeline.
CircleCI example of a scheduled workflow:
```yaml
commit-workflow:
jobs:
- build
scheduled-workflow:
triggers:
- schedule:
cron: "0 1 * * *"
filters:
branches:
only: try-schedule-workflow
jobs:
- build
```
Example of the same scheduled pipeline using [`rules`](../yaml/README.md#rules) in GitLab CI/CD:
```yaml
job1:
script:
- make build
rules:
- if: '$CI_PIPELINE_SOURCE == "schedule" && $CI_COMMIT_REF_NAME == "try-schedule-workflow"'
```
After the pipeline configuration is saved, you configure the cron schedule in the [GitLab UI](../pipelines/schedules.md#configuring-pipeline-schedules), and can enable or disable schedules in the UI as well.
#### Manual run
CircleCI example of a manual workflow:
```yaml
release-branch-workflow:
jobs:
- build
- testing:
requires:
- build
- deploy:
type: approval
requires:
- testing
```
Example of the same workflow using [`when: manual`](../yaml/README.md#whenmanual) in GitLab CI/CD:
```yaml
deploy_prod:
stage: deploy
script:
- echo "Deploy to production server"
when: manual
```
### Filter job by branch
[Rules](../yaml/README.md#rules) are a mechanism to determine if the job will or will not run for a specific branch.
CircleCI example of a job filtered by branch:
```yaml
jobs:
deploy:
branches:
only:
- master
- /rc-.*/
```
Example of the same workflow using `rules` in GitLab CI/CD:
```yaml
deploy_prod:
stage: deploy
script:
- echo "Deploy to production server"
rules:
- if: '$CI_COMMIT_BRANCH == "master"'
```
### Caching
GitLab provides a caching mechanism to speed up build times for your jobs by reusing previously downloaded dependencies. It's important to know the different between [cache and artifacts](../caching/index.md#cache-vs-artifacts) to make the best use of these features.
CircleCI example of a job using a cache:
```yaml
jobs:
job1:
steps:
- restore_cache:
key: source-v1-< .Revision >
- checkout
- run: npm install
- save_cache:
key: source-v1-< .Revision >
paths:
- "node_modules"
```
Example of the same pipeline using `cache` in GitLab CI/CD:
```yaml
image: node:latest
# Cache modules in between jobs
cache:
key: $CI_COMMIT_REF_SLUG
paths:
- .npm/
before_script:
- npm ci --cache .npm --prefer-offline
test_async:
script:
- node ./specs/start.js ./specs/async.spec.js
```
## Contexts and variables
CircleCI provides [Contexts](https://circleci.com/docs/2.0/contexts/) to securely pass environment variables across project pipelines. In GitLab, a [Group](../../user/group/index.md) can be created to assemble related projects together. At the group level, [variables](../variables/README.md#group-level-environment-variables) can be stored outside the individual projects, and securely passed into pipelines across multiple projects.
## Orbs
There are two GitLab issues open addressing CircleCI Orbs and how GitLab can achieve similar functionality.
- <https://gitlab.com/gitlab-com/Product/-/issues/1151>
- <https://gitlab.com/gitlab-org/gitlab/-/issues/195173>
## Build environments
CircleCI offers `executors` as the underlying technology to run a specific job. In GitLab, this is done by [Runners](https://docs.gitlab.com/runner/).
The following environments are supported:
Self-Managed Runners:
- Linux
- Windows
- macOS
GitLab.com Shared Runners:
- Linux
- Windows
- [Planned: macOS](https://gitlab.com/gitlab-com/gl-infra/infrastructure/-/issues/5720)
### Machine and specific build environments
[Tags](../yaml/README.md#tags) can be used to run jobs on different platforms, by telling GitLab which Runners should run the jobs.
CircleCI example of a job running on a specific environment:
```yaml
jobs:
ubuntuJob:
machine:
image: ubuntu-1604:201903-01
steps:
- checkout
- run: echo "Hello, $USER!"
osxJob:
macos:
xcode: 11.3.0
steps:
- checkout
- run: echo "Hello, $USER!"
```
Example of the same job using `tags` in GitLab CI/CD:
```yaml
windows job:
stage:
- build
tags:
- windows
script:
- echo Hello, %USERNAME%!
osx job:
stage:
- build
tags:
- osx
script:
- echo "Hello, $USER!"
```

View File

@ -22,6 +22,11 @@ This guide will help you get started with Git through the command line and can b
for Git commands in the future. If you're only looking for a quick reference of Git commands, you for Git commands in the future. If you're only looking for a quick reference of Git commands, you
can download GitLab's [Git Cheat Sheet](https://about.gitlab.com/images/press/git-cheat-sheet.pdf). can download GitLab's [Git Cheat Sheet](https://about.gitlab.com/images/press/git-cheat-sheet.pdf).
> For more information about the advantages of working with Git and GitLab:
>
> - Watch the [GitLab Source Code Management Walkthrough](https://www.youtube.com/watch?v=wTQ3aXJswtM) video.
> - Learn how GitLab became the backbone of [Worldline](https://about.gitlab.com/customers/worldline/)s development environment.
TIP: **Tip:** TIP: **Tip:**
To help you visualize what you're doing locally, there are To help you visualize what you're doing locally, there are
[Git GUI apps](https://git-scm.com/download/gui/) you can install. [Git GUI apps](https://git-scm.com/download/gui/) you can install.

View File

@ -26,6 +26,11 @@ This means that until Git automatically cleans detached commits (which cannot be
accessed by branch or tag) it will be possible to view them with `git reflog` command accessed by branch or tag) it will be possible to view them with `git reflog` command
and access them with direct commit ID. Read more about _[redoing the undo](#redoing-the-undo)_ in the section below. and access them with direct commit ID. Read more about _[redoing the undo](#redoing-the-undo)_ in the section below.
> For more information about working with Git and GitLab:
>
> - Learn why [North Western Mutual chose GitLab](https://youtu.be/kPNMyxKRRoM) for their Enterprise source code management.
> - Learn how to [get started with Git](https://about.gitlab.com/resources/whitepaper-moving-to-git/).
## Introduction ## Introduction
This guide is organized depending on the [stage of development](https://git-scm.com/book/en/v2/Git-Basics-Recording-Changes-to-the-Repository) This guide is organized depending on the [stage of development](https://git-scm.com/book/en/v2/Git-Basics-Recording-Changes-to-the-Repository)

View File

@ -0,0 +1,41 @@
# frozen_string_literal: true
module Gitlab
module Metrics
# Rack middleware for tracking Elasticsearch metrics from Grape and Web requests.
class ElasticsearchRackMiddleware
HISTOGRAM_BUCKETS = [0.1, 0.25, 0.5, 1, 2.5, 5, 10, 60].freeze
def initialize(app)
@app = app
@requests_total_counter = Gitlab::Metrics.counter(:http_elasticsearch_requests_total,
'Amount of calls to Elasticsearch servers during web requests',
Gitlab::Metrics::Transaction::BASE_LABELS)
@requests_duration_histogram = Gitlab::Metrics.histogram(:http_elasticsearch_requests_duration_seconds,
'Query time for Elasticsearch servers during web requests',
Gitlab::Metrics::Transaction::BASE_LABELS,
HISTOGRAM_BUCKETS)
end
def call(env)
transaction = Gitlab::Metrics.current_transaction
@app.call(env)
ensure
record_metrics(transaction)
end
private
def record_metrics(transaction)
labels = transaction.labels
query_time = ::Gitlab::Instrumentation::ElasticsearchTransport.query_time
request_count = ::Gitlab::Instrumentation::ElasticsearchTransport.get_request_count
@requests_total_counter.increment(labels, request_count)
@requests_duration_histogram.observe(labels, query_time)
end
end
end
end

View File

@ -6,6 +6,14 @@ module Gitlab
class RedisRackMiddleware class RedisRackMiddleware
def initialize(app) def initialize(app)
@app = app @app = app
@requests_total_counter = Gitlab::Metrics.counter(:http_redis_requests_total,
'Amount of calls to Redis servers during web requests',
Gitlab::Metrics::Transaction::BASE_LABELS)
@requests_duration_histogram = Gitlab::Metrics.histogram(:http_redis_requests_duration_seconds,
'Query time for Redis servers during web requests',
Gitlab::Metrics::Transaction::BASE_LABELS,
Gitlab::Instrumentation::Redis::QUERY_TIME_BUCKETS)
end end
def call(env) def call(env)
@ -13,7 +21,7 @@ module Gitlab
@app.call(env) @app.call(env)
ensure ensure
record_metrics(transaction) if transaction record_metrics(transaction)
end end
private private
@ -23,14 +31,8 @@ module Gitlab
query_time = Gitlab::Instrumentation::Redis.query_time query_time = Gitlab::Instrumentation::Redis.query_time
request_count = Gitlab::Instrumentation::Redis.get_request_count request_count = Gitlab::Instrumentation::Redis.get_request_count
Gitlab::Metrics.counter(:http_redis_requests_total, @requests_total_counter.increment(labels, request_count)
'Amount of calls to Redis servers during web requests', @requests_duration_histogram.observe(labels, query_time)
Gitlab::Metrics::Transaction::BASE_LABELS).increment(labels, request_count)
Gitlab::Metrics.histogram(:http_redis_requests_duration_seconds,
'Query time for Redis servers during web requests',
Gitlab::Metrics::Transaction::BASE_LABELS,
Gitlab::Instrumentation::Redis::QUERY_TIME_BUCKETS).observe(labels, query_time)
end end
end end
end end

View File

@ -79,6 +79,7 @@ module Gitlab
config[:'gitaly-ruby'] = { dir: File.join(gitaly_dir, 'ruby') } if gitaly_ruby config[:'gitaly-ruby'] = { dir: File.join(gitaly_dir, 'ruby') } if gitaly_ruby
config[:'gitlab-shell'] = { dir: Gitlab.config.gitlab_shell.path } config[:'gitlab-shell'] = { dir: Gitlab.config.gitlab_shell.path }
config[:bin_dir] = Gitlab.config.gitaly.client_path config[:bin_dir] = Gitlab.config.gitaly.client_path
config[:gitlab] = { url: Gitlab.config.gitlab.url }
TomlRB.dump(config) TomlRB.dump(config)
end end
@ -97,7 +98,8 @@ module Gitlab
def configuration_toml(gitaly_dir, storage_paths) def configuration_toml(gitaly_dir, storage_paths)
nodes = [{ storage: 'default', address: "unix:#{gitaly_dir}/gitaly.socket", primary: true, token: 'secret' }] nodes = [{ storage: 'default', address: "unix:#{gitaly_dir}/gitaly.socket", primary: true, token: 'secret' }]
storages = [{ name: 'default', node: nodes }] storages = [{ name: 'default', node: nodes }]
config = { socket_path: "#{gitaly_dir}/praefect.socket", memory_queue_enabled: true, virtual_storage: storages } failover = { enabled: false }
config = { socket_path: "#{gitaly_dir}/praefect.socket", memory_queue_enabled: true, virtual_storage: storages, failover: failover }
config[:token] = 'secret' if Rails.env.test? config[:token] = 'secret' if Rails.env.test?
TomlRB.dump(config) TomlRB.dump(config)

View File

@ -983,9 +983,6 @@ msgstr ""
msgid "ACTION REQUIRED: Something went wrong while obtaining the Let's Encrypt certificate for GitLab Pages domain '%{domain}'" msgid "ACTION REQUIRED: Something went wrong while obtaining the Let's Encrypt certificate for GitLab Pages domain '%{domain}'"
msgstr "" msgstr ""
msgid "API Help"
msgstr ""
msgid "API Token" msgid "API Token"
msgstr "" msgstr ""
@ -1441,9 +1438,6 @@ msgstr ""
msgid "Admin Overview" msgid "Admin Overview"
msgstr "" msgstr ""
msgid "Admin Section"
msgstr ""
msgid "Admin mode already enabled" msgid "Admin mode already enabled"
msgstr "" msgstr ""
@ -7487,9 +7481,6 @@ msgstr ""
msgid "DesignManagement|%{filename} did not change." msgid "DesignManagement|%{filename} did not change."
msgstr "" msgstr ""
msgid "DesignManagement|Add designs"
msgstr ""
msgid "DesignManagement|Adding a design with the same filename replaces the file in a new version." msgid "DesignManagement|Adding a design with the same filename replaces the file in a new version."
msgstr "" msgstr ""
@ -7574,6 +7565,9 @@ msgstr ""
msgid "DesignManagement|To enable design management, you'll need to %{requirements_link_start}meet the requirements%{requirements_link_end}. If you need help, reach out to our %{support_link_start}support team%{support_link_end} for assistance." msgid "DesignManagement|To enable design management, you'll need to %{requirements_link_start}meet the requirements%{requirements_link_end}. If you need help, reach out to our %{support_link_start}support team%{support_link_end} for assistance."
msgstr "" msgstr ""
msgid "DesignManagement|Upload designs"
msgstr ""
msgid "DesignManagement|Upload skipped." msgid "DesignManagement|Upload skipped."
msgstr "" msgstr ""
@ -13336,9 +13330,6 @@ msgstr ""
msgid "Markdown" msgid "Markdown"
msgstr "" msgstr ""
msgid "Markdown Help"
msgstr ""
msgid "Markdown enabled" msgid "Markdown enabled"
msgstr "" msgstr ""
@ -15654,9 +15645,6 @@ msgstr ""
msgid "Permissions" msgid "Permissions"
msgstr "" msgstr ""
msgid "Permissions Help"
msgstr ""
msgid "Permissions, LFS, 2FA" msgid "Permissions, LFS, 2FA"
msgstr "" msgstr ""
@ -17685,9 +17673,6 @@ msgstr ""
msgid "Public - The project can be accessed without any authentication." msgid "Public - The project can be accessed without any authentication."
msgstr "" msgstr ""
msgid "Public Access Help"
msgstr ""
msgid "Public deploy keys (%{deploy_keys_count})" msgid "Public deploy keys (%{deploy_keys_count})"
msgstr "" msgstr ""
@ -17817,9 +17802,6 @@ msgstr ""
msgid "README" msgid "README"
msgstr "" msgstr ""
msgid "Rake Tasks Help"
msgstr ""
msgid "Raw blob request rate limit per minute" msgid "Raw blob request rate limit per minute"
msgstr "" msgstr ""
@ -18861,9 +18843,6 @@ msgstr ""
msgid "SSH Keys" msgid "SSH Keys"
msgstr "" msgstr ""
msgid "SSH Keys Help"
msgstr ""
msgid "SSH host key fingerprints" msgid "SSH host key fingerprints"
msgstr "" msgstr ""
@ -21343,9 +21322,6 @@ msgstr ""
msgid "System Hooks" msgid "System Hooks"
msgstr "" msgstr ""
msgid "System Hooks Help"
msgstr ""
msgid "System Info" msgid "System Info"
msgstr "" msgstr ""
@ -23975,9 +23951,6 @@ msgstr ""
msgid "User restrictions" msgid "User restrictions"
msgstr "" msgstr ""
msgid "User settings"
msgstr ""
msgid "User was successfully created." msgid "User was successfully created."
msgstr "" msgstr ""
@ -24789,9 +24762,6 @@ msgstr ""
msgid "Webhooks" msgid "Webhooks"
msgstr "" msgstr ""
msgid "Webhooks Help"
msgstr ""
msgid "Webhooks allow you to trigger a URL if, for example, new code is pushed or a new issue is created. You can configure webhooks to listen for specific events like pushes, issues or merge requests. Group webhooks will apply to all projects in a group, allowing you to standardize webhook functionality across your entire group." msgid "Webhooks allow you to trigger a URL if, for example, new code is pushed or a new issue is created. You can configure webhooks to listen for specific events like pushes, issues or merge requests. Group webhooks will apply to all projects in a group, allowing you to standardize webhook functionality across your entire group."
msgstr "" msgstr ""
@ -25058,9 +25028,6 @@ msgstr ""
msgid "Work in progress Limit" msgid "Work in progress Limit"
msgstr "" msgstr ""
msgid "Workflow Help"
msgstr ""
msgid "Write" msgid "Write"
msgstr "" msgstr ""

View File

@ -1,64 +0,0 @@
# frozen_string_literal: true
require 'rubocop/rspec/final_end_location'
require 'rubocop/rspec/blank_line_separation'
require 'rubocop/rspec/language'
module RuboCop
module Cop
module RSpec
# Checks if there is an empty line after shared example blocks.
#
# @example
# # bad
# RSpec.describe Foo do
# it_behaves_like 'do this first'
# it_behaves_like 'does this' do
# end
# it_behaves_like 'does that' do
# end
# it_behaves_like 'do some more'
# end
#
# # good
# RSpec.describe Foo do
# it_behaves_like 'do this first'
# it_behaves_like 'does this' do
# end
#
# it_behaves_like 'does that' do
# end
#
# it_behaves_like 'do some more'
# end
#
# # fair - it's ok to have non-separated without blocks
# RSpec.describe Foo do
# it_behaves_like 'do this first'
# it_behaves_like 'does this'
# end
#
class EmptyLineAfterSharedExample < RuboCop::Cop::Cop
include RuboCop::RSpec::BlankLineSeparation
include RuboCop::RSpec::Language
MSG = 'Add an empty line after `%<example>s` block.'
def_node_matcher :shared_examples,
(SharedGroups::ALL + Includes::ALL).block_pattern
def on_block(node)
shared_examples(node) do
break if last_child?(node)
missing_separating_line(node) do |location|
add_offense(node,
location: location,
message: format(MSG, example: node.method_name))
end
end
end
end
end
end
end

View File

@ -14,6 +14,7 @@ class GitalyTestBuild
def run def run
abort 'gitaly build failed' unless system(env, 'make', chdir: tmp_tests_gitaly_dir) abort 'gitaly build failed' unless system(env, 'make', chdir: tmp_tests_gitaly_dir)
ensure_gitlab_shell_secret!
check_gitaly_config! check_gitaly_config!
# Starting gitaly further validates its configuration # Starting gitaly further validates its configuration

View File

@ -4,6 +4,7 @@
# Please be careful when modifying this file. Your changes must work # Please be careful when modifying this file. Your changes must work
# both for local development rspec runs, and in CI. # both for local development rspec runs, and in CI.
require 'securerandom'
require 'socket' require 'socket'
module GitalyTest module GitalyTest
@ -11,10 +12,22 @@ module GitalyTest
File.expand_path('../tmp/tests/gitaly', __dir__) File.expand_path('../tmp/tests/gitaly', __dir__)
end end
def tmp_tests_gitlab_shell_dir
File.expand_path('../tmp/tests/gitlab-shell', __dir__)
end
def rails_gitlab_shell_secret
File.expand_path('../.gitlab_shell_secret', __dir__)
end
def gemfile def gemfile
File.join(tmp_tests_gitaly_dir, 'ruby', 'Gemfile') File.join(tmp_tests_gitaly_dir, 'ruby', 'Gemfile')
end end
def gitlab_shell_secret_file
File.join(tmp_tests_gitlab_shell_dir, '.gitlab_shell_secret')
end
def env def env
env_hash = { env_hash = {
'HOME' => File.expand_path('tmp/tests'), 'HOME' => File.expand_path('tmp/tests'),
@ -70,6 +83,20 @@ module GitalyTest
pid pid
end end
# Taken from Gitlab::Shell.generate_and_link_secret_token
def ensure_gitlab_shell_secret!
secret_file = rails_gitlab_shell_secret
shell_link = gitlab_shell_secret_file
unless File.size?(secret_file)
File.write(secret_file, SecureRandom.hex(16))
end
unless File.exist?(shell_link)
FileUtils.ln_s(secret_file, shell_link)
end
end
def check_gitaly_config! def check_gitaly_config!
puts "Checking gitaly-ruby Gemfile..." puts "Checking gitaly-ruby Gemfile..."

View File

@ -211,9 +211,4 @@ describe SearchController do
end.to raise_error(ActionController::ParameterMissing) end.to raise_error(ActionController::ParameterMissing)
end end
end end
describe 'GET #autocomplete' do
it_behaves_like 'when the user cannot read cross project', :autocomplete, { term: 'hello' }
it_behaves_like 'with external authorization service enabled', :autocomplete, { term: 'hello' }
end
end end

View File

@ -13,4 +13,6 @@ require 'active_support/all'
ActiveSupport::Dependencies.autoload_paths << 'lib' ActiveSupport::Dependencies.autoload_paths << 'lib'
ActiveSupport::Dependencies.autoload_paths << 'ee/lib' ActiveSupport::Dependencies.autoload_paths << 'ee/lib'
ActiveSupport::Dependencies.autoload_paths << 'tooling/lib'
ActiveSupport::XmlMini.backend = 'Nokogiri' ActiveSupport::XmlMini.backend = 'Nokogiri'

View File

@ -10,7 +10,7 @@ exports[`Design management upload button component renders inverted upload desig
variant="success" variant="success"
> >
Add designs Upload designs
<!----> <!---->
</gl-deprecated-button-stub> </gl-deprecated-button-stub>
@ -34,7 +34,7 @@ exports[`Design management upload button component renders loading icon 1`] = `
variant="success" variant="success"
> >
Add designs Upload designs
<gl-loading-icon-stub <gl-loading-icon-stub
class="ml-1" class="ml-1"
@ -63,7 +63,7 @@ exports[`Design management upload button component renders upload design button
variant="success" variant="success"
> >
Add designs Upload designs
<!----> <!---->
</gl-deprecated-button-stub> </gl-deprecated-button-stub>

View File

@ -8,99 +8,6 @@ describe SearchHelper do
str str
end end
describe 'search_autocomplete_opts' do
context "with no current user" do
before do
allow(self).to receive(:current_user).and_return(nil)
end
it "returns nil" do
expect(search_autocomplete_opts("q")).to be_nil
end
end
context "with a standard user" do
let(:user) { create(:user) }
before do
allow(self).to receive(:current_user).and_return(user)
end
it "includes Help sections" do
expect(search_autocomplete_opts("hel").size).to eq(9)
end
it "includes default sections" do
expect(search_autocomplete_opts("dash").size).to eq(1)
end
it "does not include admin sections" do
expect(search_autocomplete_opts("admin").size).to eq(0)
end
it "does not allow regular expression in search term" do
expect(search_autocomplete_opts("(webhooks|api)").size).to eq(0)
end
it "includes the user's groups" do
create(:group).add_owner(user)
expect(search_autocomplete_opts("gro").size).to eq(1)
end
it "includes nested group" do
create(:group, :nested, name: 'foo').add_owner(user)
expect(search_autocomplete_opts('foo').size).to eq(1)
end
it "includes the user's projects" do
project = create(:project, namespace: create(:namespace, owner: user))
expect(search_autocomplete_opts(project.name).size).to eq(1)
end
it "includes the required project attrs" do
project = create(:project, namespace: create(:namespace, owner: user))
result = search_autocomplete_opts(project.name).first
expect(result.keys).to match_array(%i[category id value label url avatar_url])
end
it "includes the required group attrs" do
create(:group).add_owner(user)
result = search_autocomplete_opts("gro").first
expect(result.keys).to match_array(%i[category id label url avatar_url])
end
it "does not include the public group" do
group = create(:group)
expect(search_autocomplete_opts(group.name).size).to eq(0)
end
context "with a current project" do
before do
@project = create(:project, :repository)
end
it "includes project-specific sections" do
expect(search_autocomplete_opts("Files").size).to eq(1)
expect(search_autocomplete_opts("Commits").size).to eq(1)
end
end
end
context 'with an admin user' do
let(:admin) { create(:admin) }
before do
allow(self).to receive(:current_user).and_return(admin)
end
it "includes admin sections" do
expect(search_autocomplete_opts("admin").size).to eq(1)
end
end
end
describe 'search_entries_info' do describe 'search_entries_info' do
using RSpec::Parameterized::TableSyntax using RSpec::Parameterized::TableSyntax

View File

@ -2,10 +2,10 @@
import $ from 'jquery'; import $ from 'jquery';
import '~/gl_dropdown'; import '~/gl_dropdown';
import initSearchAutocomplete from '~/search_autocomplete'; import initGlobalSearchInput from '~/global_search_input';
import '~/lib/utils/common_utils'; import '~/lib/utils/common_utils';
describe('Search autocomplete dropdown', () => { describe('Global search input dropdown', () => {
let widget = null; let widget = null;
const userName = 'root'; const userName = 'root';
@ -112,15 +112,15 @@ describe('Search autocomplete dropdown', () => {
expect(list.find(mrsIHaveCreatedLink).text()).toBe("Merge requests I've created"); expect(list.find(mrsIHaveCreatedLink).text()).toBe("Merge requests I've created");
}; };
preloadFixtures('static/search_autocomplete.html'); preloadFixtures('static/global_search_input.html');
beforeEach(function() { beforeEach(function() {
loadFixtures('static/search_autocomplete.html'); loadFixtures('static/global_search_input.html');
window.gon = {}; window.gon = {};
window.gon.current_user_id = userId; window.gon.current_user_id = userId;
window.gon.current_username = userName; window.gon.current_username = userName;
return (widget = initSearchAutocomplete()); return (widget = initGlobalSearchInput());
}); });
afterEach(function() { afterEach(function() {
@ -189,25 +189,25 @@ describe('Search autocomplete dropdown', () => {
expect(submitSpy).not.toHaveBeenTriggered(); expect(submitSpy).not.toHaveBeenTriggered();
}); });
describe('disableAutocomplete', function() { describe('disableDropdown', function() {
beforeEach(function() { beforeEach(function() {
widget.enableAutocomplete(); widget.enableDropdown();
}); });
it('should close the Dropdown', function() { it('should close the Dropdown', function() {
const toggleSpy = spyOn(widget.dropdownToggle, 'dropdown'); const toggleSpy = spyOn(widget.dropdownToggle, 'dropdown');
widget.dropdown.addClass('show'); widget.dropdown.addClass('show');
widget.disableAutocomplete(); widget.disableDropdown();
expect(toggleSpy).toHaveBeenCalledWith('toggle'); expect(toggleSpy).toHaveBeenCalledWith('toggle');
}); });
}); });
describe('enableAutocomplete', function() { describe('enableDropdown', function() {
it('should open the Dropdown', function() { it('should open the Dropdown', function() {
const toggleSpy = spyOn(widget.dropdownToggle, 'dropdown'); const toggleSpy = spyOn(widget.dropdownToggle, 'dropdown');
widget.enableAutocomplete(); widget.enableDropdown();
expect(toggleSpy).toHaveBeenCalledWith('toggle'); expect(toggleSpy).toHaveBeenCalledWith('toggle');
}); });

View File

@ -0,0 +1,57 @@
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Metrics::ElasticsearchRackMiddleware do
let(:app) { double(:app, call: 'app call result') }
let(:middleware) { described_class.new(app) }
let(:env) { {} }
let(:transaction) { Gitlab::Metrics::WebTransaction.new(env) }
describe '#call' do
let(:counter) { instance_double(Prometheus::Client::Counter, increment: nil) }
let(:histogram) { instance_double(Prometheus::Client::Histogram, observe: nil) }
let(:elasticsearch_query_time) { 0.1 }
let(:elasticsearch_requests_count) { 2 }
before do
allow(Gitlab::Instrumentation::ElasticsearchTransport).to receive(:query_time) { elasticsearch_query_time }
allow(Gitlab::Instrumentation::ElasticsearchTransport).to receive(:get_request_count) { elasticsearch_requests_count }
allow(Gitlab::Metrics).to receive(:counter)
.with(:http_elasticsearch_requests_total,
an_instance_of(String),
Gitlab::Metrics::Transaction::BASE_LABELS)
.and_return(counter)
allow(Gitlab::Metrics).to receive(:histogram)
.with(:http_elasticsearch_requests_duration_seconds,
an_instance_of(String),
Gitlab::Metrics::Transaction::BASE_LABELS,
described_class::HISTOGRAM_BUCKETS)
.and_return(histogram)
allow(Gitlab::Metrics).to receive(:current_transaction).and_return(transaction)
end
it 'calls the app' do
expect(middleware.call(env)).to eq('app call result')
end
it 'records elasticsearch metrics' do
expect(counter).to receive(:increment).with(transaction.labels, elasticsearch_requests_count)
expect(histogram).to receive(:observe).with(transaction.labels, elasticsearch_query_time)
middleware.call(env)
end
it 'records elasticsearch metrics if an error is raised' do
expect(counter).to receive(:increment).with(transaction.labels, elasticsearch_requests_count)
expect(histogram).to receive(:observe).with(transaction.labels, elasticsearch_query_time)
allow(app).to receive(:call).with(env).and_raise(StandardError)
expect { middleware.call(env) }.to raise_error(StandardError)
end
end
end

View File

@ -13,68 +13,49 @@ describe Gitlab::Metrics::RedisRackMiddleware do
end end
describe '#call' do describe '#call' do
context 'when metrics are disabled' do let(:counter) { double(Prometheus::Client::Counter, increment: nil) }
before do let(:histogram) { double(Prometheus::Client::Histogram, observe: nil) }
allow(Gitlab::Metrics).to receive(:current_transaction).and_return(nil) let(:redis_query_time) { 0.1 }
end let(:redis_requests_count) { 2 }
it 'calls the app' do before do
expect(middleware.call(env)).to eq('wub wub') allow(Gitlab::Instrumentation::Redis).to receive(:query_time) { redis_query_time }
end allow(Gitlab::Instrumentation::Redis).to receive(:get_request_count) { redis_requests_count }
it 'does not record metrics' do allow(Gitlab::Metrics).to receive(:counter)
expect(Gitlab::Metrics).not_to receive(:counter) .with(:http_redis_requests_total,
expect(Gitlab::Metrics).not_to receive(:histogram) an_instance_of(String),
Gitlab::Metrics::Transaction::BASE_LABELS)
.and_return(counter)
middleware.call(env) allow(Gitlab::Metrics).to receive(:histogram)
end .with(:http_redis_requests_duration_seconds,
an_instance_of(String),
Gitlab::Metrics::Transaction::BASE_LABELS,
Gitlab::Instrumentation::Redis::QUERY_TIME_BUCKETS)
.and_return(histogram)
allow(Gitlab::Metrics).to receive(:current_transaction).and_return(transaction)
end end
context 'when metrics are enabled' do it 'calls the app' do
let(:counter) { double(Prometheus::Client::Counter, increment: nil) } expect(middleware.call(env)).to eq('wub wub')
let(:histogram) { double(Prometheus::Client::Histogram, observe: nil) } end
let(:redis_query_time) { 0.1 }
let(:redis_requests_count) { 2 }
before do it 'records redis metrics' do
allow(Gitlab::Instrumentation::Redis).to receive(:query_time) { redis_query_time } expect(counter).to receive(:increment).with(transaction.labels, redis_requests_count)
allow(Gitlab::Instrumentation::Redis).to receive(:get_request_count) { redis_requests_count } expect(histogram).to receive(:observe).with(transaction.labels, redis_query_time)
allow(Gitlab::Metrics).to receive(:counter) middleware.call(env)
.with(:http_redis_requests_total, end
an_instance_of(String),
Gitlab::Metrics::Transaction::BASE_LABELS)
.and_return(counter)
allow(Gitlab::Metrics).to receive(:histogram) it 'records redis metrics if an error is raised' do
.with(:http_redis_requests_duration_seconds, expect(counter).to receive(:increment).with(transaction.labels, redis_requests_count)
an_instance_of(String), expect(histogram).to receive(:observe).with(transaction.labels, redis_query_time)
Gitlab::Metrics::Transaction::BASE_LABELS,
Gitlab::Instrumentation::Redis::QUERY_TIME_BUCKETS)
.and_return(histogram)
allow(Gitlab::Metrics).to receive(:current_transaction).and_return(transaction) allow(app).to receive(:call).with(env).and_raise(StandardError)
end
it 'calls the app' do expect { middleware.call(env) }.to raise_error(StandardError)
expect(middleware.call(env)).to eq('wub wub')
end
it 'records redis metrics' do
expect(counter).to receive(:increment).with(transaction.labels, redis_requests_count)
expect(histogram).to receive(:observe).with(transaction.labels, redis_query_time)
middleware.call(env)
end
it 'records redis metrics if an error is raised' do
expect(counter).to receive(:increment).with(transaction.labels, redis_requests_count)
expect(histogram).to receive(:observe).with(transaction.labels, redis_query_time)
allow(app).to receive(:call).with(env).and_raise(StandardError)
expect { middleware.call(env) }.to raise_error(StandardError)
end
end end
end end
end end

View File

@ -1,86 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
require_relative '../../../../rubocop/cop/rspec/empty_line_after_shared_example'
describe RuboCop::Cop::RSpec::EmptyLineAfterSharedExample do
subject(:cop) { described_class.new }
it 'flags a missing empty line after `it_behaves_like` block' do
expect_offense(<<-RUBY)
RSpec.describe Foo do
it_behaves_like 'does this' do
end
^^^ Add an empty line after `it_behaves_like` block.
it_behaves_like 'does that' do
end
end
RUBY
expect_correction(<<-RUBY)
RSpec.describe Foo do
it_behaves_like 'does this' do
end
it_behaves_like 'does that' do
end
end
RUBY
end
it 'ignores one-line shared examples before shared example blocks' do
expect_no_offenses(<<-RUBY)
RSpec.describe Foo do
it_behaves_like 'does this'
it_behaves_like 'does that' do
end
end
RUBY
end
it 'flags a missing empty line after `shared_examples`' do
expect_offense(<<-RUBY)
RSpec.context 'foo' do
shared_examples do
end
^^^ Add an empty line after `shared_examples` block.
shared_examples 'something gets done' do
end
end
RUBY
expect_correction(<<-RUBY)
RSpec.context 'foo' do
shared_examples do
end
shared_examples 'something gets done' do
end
end
RUBY
end
it 'ignores consecutive one-liners' do
expect_no_offenses(<<-RUBY)
RSpec.describe Foo do
it_behaves_like 'do this'
it_behaves_like 'do that'
end
RUBY
end
it 'flags mixed one-line and multi-line shared examples' do
expect_offense(<<-RUBY)
RSpec.context 'foo' do
it_behaves_like 'do this'
it_behaves_like 'do that'
it_behaves_like 'does this' do
end
^^^ Add an empty line after `it_behaves_like` block.
it_behaves_like 'do this'
it_behaves_like 'do that'
end
RUBY
end
end

View File

@ -82,9 +82,9 @@ describe AutoMerge::BaseService do
end end
end end
context 'when failed to save' do context 'when failed to save merge request' do
before do before do
allow(merge_request).to receive(:save) { false } allow(merge_request).to receive(:save!) { raise ActiveRecord::RecordInvalid.new }
end end
it 'does not yield block' do it 'does not yield block' do
@ -94,6 +94,39 @@ describe AutoMerge::BaseService do
it 'returns failed' do it 'returns failed' do
is_expected.to eq(:failed) is_expected.to eq(:failed)
end end
it 'tracks the exception' do
expect(Gitlab::ErrorTracking)
.to receive(:track_exception).with(kind_of(ActiveRecord::RecordInvalid),
merge_request_id: merge_request.id)
subject
end
end
context 'when exception happens in yield block' do
def execute_with_error_in_yield
service.execute(merge_request) { raise 'Something went wrong' }
end
it 'returns failed status' do
expect(execute_with_error_in_yield).to eq(:failed)
end
it 'rollback the transaction' do
execute_with_error_in_yield
merge_request.reload
expect(merge_request).not_to be_auto_merge_enabled
end
it 'tracks the exception' do
expect(Gitlab::ErrorTracking)
.to receive(:track_exception).with(kind_of(RuntimeError),
merge_request_id: merge_request.id)
execute_with_error_in_yield
end
end end
end end
@ -162,7 +195,7 @@ describe AutoMerge::BaseService do
context 'when failed to save' do context 'when failed to save' do
before do before do
allow(merge_request).to receive(:save) { false } allow(merge_request).to receive(:save!) { raise ActiveRecord::RecordInvalid.new }
end end
it 'does not yield block' do it 'does not yield block' do
@ -178,9 +211,9 @@ describe AutoMerge::BaseService do
it_behaves_like 'Canceled or Dropped' it_behaves_like 'Canceled or Dropped'
context 'when failed to save' do context 'when failed to save merge request' do
before do before do
allow(merge_request).to receive(:save) { false } allow(merge_request).to receive(:save!) { raise ActiveRecord::RecordInvalid.new }
end end
it 'returns error status' do it 'returns error status' do
@ -188,6 +221,33 @@ describe AutoMerge::BaseService do
expect(subject[:message]).to eq("Can't cancel the automatic merge") expect(subject[:message]).to eq("Can't cancel the automatic merge")
end end
end end
context 'when exception happens in yield block' do
def cancel_with_error_in_yield
service.cancel(merge_request) { raise 'Something went wrong' }
end
it 'returns error' do
result = cancel_with_error_in_yield
expect(result[:status]).to eq(:error)
expect(result[:message]).to eq("Can't cancel the automatic merge")
end
it 'rollback the transaction' do
cancel_with_error_in_yield
merge_request.reload
expect(merge_request).to be_auto_merge_enabled
end
it 'tracks the exception' do
expect(Gitlab::ErrorTracking)
.to receive(:track_exception).with(kind_of(RuntimeError),
merge_request_id: merge_request.id)
cancel_with_error_in_yield
end
end
end end
describe '#abort' do describe '#abort' do
@ -200,7 +260,7 @@ describe AutoMerge::BaseService do
context 'when failed to save' do context 'when failed to save' do
before do before do
allow(merge_request).to receive(:save) { false } allow(merge_request).to receive(:save!) { raise ActiveRecord::RecordInvalid.new }
end end
it 'returns error status' do it 'returns error status' do
@ -208,5 +268,32 @@ describe AutoMerge::BaseService do
expect(subject[:message]).to eq("Can't abort the automatic merge") expect(subject[:message]).to eq("Can't abort the automatic merge")
end end
end end
context 'when exception happens in yield block' do
def abort_with_error_in_yield
service.abort(merge_request, reason) { raise 'Something went wrong' }
end
it 'returns error' do
result = abort_with_error_in_yield
expect(result[:status]).to eq(:error)
expect(result[:message]).to eq("Can't abort the automatic merge")
end
it 'rollback the transaction' do
abort_with_error_in_yield
merge_request.reload
expect(merge_request).to be_auto_merge_enabled
end
it 'tracks the exception' do
expect(Gitlab::ErrorTracking)
.to receive(:track_exception).with(kind_of(RuntimeError),
merge_request_id: merge_request.id)
abort_with_error_in_yield
end
end
end end
end end

View File

@ -339,29 +339,40 @@ describe Projects::CreateService, '#execute' do
end end
end end
context 'when there is an active service template' do describe 'create service for the project' do
before do subject(:project) { create_project(user, opts) }
create(:prometheus_service, project: nil, template: true, active: true)
context 'when there is an active instance-level and an active template integration' do
before do
create(:prometheus_service, :instance, api_url: 'https://prometheus.instance.com/')
create(:prometheus_service, :template, api_url: 'https://prometheus.template.com/')
end
it 'creates a service from the instance-level integration' do
expect(project.services.count).to eq(1)
expect(project.services.first.api_url).to eq('https://prometheus.instance.com/')
end
end end
it 'creates a service from this template' do context 'when there is an active service template' do
project = create_project(user, opts) before do
create(:prometheus_service, :template, active: true)
end
expect(project.services.count).to eq 1 it 'creates a service from the template' do
expect(project.errors).to be_empty expect(project.services.count).to eq(1)
end
end end
end
context 'when a bad service template is created' do context 'when there is an invalid integration' do
it 'sets service to be inactive' do before do
opts[:import_url] = 'http://www.gitlab.com/gitlab-org/gitlab-foss' create(:service, :template, type: 'DroneCiService', active: true)
create(:service, type: 'DroneCiService', project: nil, template: true, active: true) end
project = create_project(user, opts) it 'creates an inactive service' do
service = project.services.first expect(project).to be_persisted
expect(project.services.first.active).to be false
expect(project).to be_persisted end
expect(service.active).to be false
end end
end end

View File

@ -0,0 +1,111 @@
# frozen_string_literal: true
require 'fast_spec_helper'
describe Tooling::TestFileFinder do
subject { Tooling::TestFileFinder.new(file) }
describe '#test_files' do
context 'when given non .rb files' do
let(:file) { 'app/assets/images/emoji.png' }
it 'does not return a test file' do
expect(subject.test_files).to be_empty
end
end
context 'when given file in app/' do
let(:file) { 'app/finders/admin/projects_finder.rb' }
it 'returns the matching app spec file' do
expect(subject.test_files).to contain_exactly('spec/finders/admin/projects_finder_spec.rb')
end
end
context 'when given file in lib/' do
let(:file) { 'lib/banzai/color_parser.rb' }
it 'returns the matching app spec file' do
expect(subject.test_files).to contain_exactly('spec/lib/banzai/color_parser_spec.rb')
end
end
context 'when given a file in tooling/' do
let(:file) { 'tooling/lib/quality/test_file_finder.rb' }
it 'returns the matching tooling test' do
expect(subject.test_files).to contain_exactly('spec/tooling/lib/quality/test_file_finder_spec.rb')
end
end
context 'when given a test file' do
let(:file) { 'spec/lib/banzai/color_parser_spec.rb' }
it 'returns the matching test file itself' do
expect(subject.test_files).to contain_exactly('spec/lib/banzai/color_parser_spec.rb')
end
end
context 'when given an app file in ee/' do
let(:file) { 'ee/app/models/analytics/cycle_analytics/group_level.rb' }
it 'returns the matching ee/ test file' do
expect(subject.test_files).to contain_exactly('ee/spec/models/analytics/cycle_analytics/group_level_spec.rb')
end
end
context 'when given a module file in ee/' do
let(:file) { 'ee/app/models/ee/user.rb' }
it 'returns the matching ee/ module test file and the ee/ model test file' do
test_files = ['ee/spec/models/ee/user_spec.rb', 'spec/app/models/user_spec.rb']
expect(subject.test_files).to contain_exactly(*test_files)
end
end
context 'when given a lib file in ee/' do
let(:file) { 'ee/lib/flipper_session.rb' }
it 'returns the matching ee/ lib test file' do
expect(subject.test_files).to contain_exactly('ee/spec/lib/flipper_session_spec.rb')
end
end
context 'when given a test file in ee/' do
let(:file) { 'ee/spec/models/container_registry/event_spec.rb' }
it 'returns the test file itself' do
expect(subject.test_files).to contain_exactly('ee/spec/models/container_registry/event_spec.rb')
end
end
context 'when given a module test file in ee/' do
let(:file) { 'ee/spec/models/ee/appearance_spec.rb' }
it 'returns the matching module test file itself and the corresponding spec model test file' do
test_files = ['ee/spec/models/ee/appearance_spec.rb', 'spec/models/appearance_spec.rb']
expect(subject.test_files).to contain_exactly(*test_files)
end
end
context 'with foss_test_only: true' do
subject { Tooling::TestFileFinder.new(file, foss_test_only: true) }
context 'when given a module file in ee/' do
let(:file) { 'ee/app/models/ee/user.rb' }
it 'returns only the corresponding spec model test file in foss' do
expect(subject.test_files).to contain_exactly('spec/app/models/user_spec.rb')
end
end
context 'when given an app file in ee/' do
let(:file) { 'ee/app/models/approval.rb' }
it 'returns no test file in foss' do
expect(subject.test_files).to be_empty
end
end
end
end
end

29
tooling/bin/find_foss_tests Executable file
View File

@ -0,0 +1,29 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
require_relative '../../lib/gitlab/popen'
require_relative '../lib/tooling/test_file_finder'
require 'gitlab'
gitlab_token = ENV.fetch('DANGER_GITLAB_API_TOKEN', '')
Gitlab.configure do |config|
config.endpoint = 'https://gitlab.com/api/v4'
config.private_token = gitlab_token
end
output_file = ARGV.shift
mr_project_path = ENV.fetch('CI_MERGE_REQUEST_PROJECT_PATH')
mr_iid = ENV.fetch('CI_MERGE_REQUEST_IID')
mr_changes = Gitlab.merge_request_changes(mr_project_path, mr_iid)
changed_files = mr_changes.changes.map { |change| change['new_path'] }
tests_to_run = changed_files.flat_map do |file|
test_files = Tooling::TestFileFinder.new(file, foss_test_only: true).test_files
test_files.select { |f| File.exist?(f) }
end
File.write(output_file, tests_to_run.uniq.join(' '))

View File

@ -0,0 +1,78 @@
# frozen_string_literal: true
require 'ostruct'
require 'set'
module Tooling
class TestFileFinder
RUBY_EXTENSION = '.rb'
EE_PREFIX = 'ee/'
def initialize(file, foss_test_only: false)
@file = file
@foss_test_only = foss_test_only
@result = Set.new
end
def test_files
contexts = [ee_context, foss_context]
contexts.flat_map do |context|
match_test_files_for(context)
end
result.to_a
end
private
attr_reader :file, :foss_test_only, :result
def ee_context
OpenStruct.new.tap do |ee|
ee.app = %r{^#{EE_PREFIX}app/(.+)\.rb$} unless foss_test_only
ee.lib = %r{^#{EE_PREFIX}lib/(.+)\.rb$} unless foss_test_only
ee.spec = %r{^#{EE_PREFIX}spec/(.+)_spec.rb$} unless foss_test_only
ee.spec_dir = "#{EE_PREFIX}spec" unless foss_test_only
ee.ee_modules = %r{^#{EE_PREFIX}(?!spec)(.*\/)ee/(.+)\.rb$}
ee.ee_module_spec = %r{^#{EE_PREFIX}spec/(.*\/)ee/(.+)\.rb$}
ee.foss_spec_dir = 'spec'
end
end
def foss_context
OpenStruct.new.tap do |foss|
foss.app = %r{^app/(.+)\.rb$}
foss.lib = %r{^lib/(.+)\.rb$}
foss.tooling = %r{^(tooling/lib/.+)\.rb$}
foss.spec = %r{^spec/(.+)_spec.rb$}
foss.spec_dir = 'spec'
end
end
def match_test_files_for(context)
if (match = context.app&.match(file))
result << "#{context.spec_dir}/#{match[1]}_spec.rb"
end
if (match = context.lib&.match(file))
result << "#{context.spec_dir}/lib/#{match[1]}_spec.rb"
end
if (match = context.tooling&.match(file))
result << "#{context.spec_dir}/#{match[1]}_spec.rb"
end
if context.spec&.match(file)
result << file
end
if (match = context.ee_modules&.match(file))
result << "#{context.foss_spec_dir}/#{match[1]}#{match[2]}_spec.rb"
end
if (match = context.ee_module_spec&.match(file))
result << "#{context.foss_spec_dir}/#{match[1]}#{match[2]}.rb"
end
end
end
end