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
.solargraph.yml
apollo.config.js
/tmp/matching_foss_tests.txt

View File

@ -324,3 +324,25 @@ db:rollback geo:
- bundle exec rake geo:db:migrate
# 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
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:
rules:
- <<: *if-merge-request

View File

@ -1 +1 @@
8.33.0
8.34.0

View File

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

View File

@ -1,10 +1,8 @@
/* eslint-disable no-return-assign, consistent-return, class-methods-use-this */
import $ from 'jquery';
import { escape, throttle } from 'lodash';
import { throttle } from 'lodash';
import { s__, __, sprintf } from '~/locale';
import { getIdenticonBackgroundClass, getIdenticonTitle } from '~/helpers/avatar_helper';
import axios from './lib/utils/axios_utils';
import {
isInGroupsPage,
isInProjectPage,
@ -67,15 +65,11 @@ function setSearchOptions() {
}
}
export class SearchAutocomplete {
constructor({ wrap, optsEl, autocompletePath, projectId, projectRef } = {}) {
export class GlobalSearchInput {
constructor({ wrap } = {}) {
setSearchOptions();
this.bindEventContext();
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.dropdownToggle = this.wrap.find('.js-dropdown-search-toggle');
this.dropdownMenu = this.dropdown.find('.dropdown-menu');
@ -92,7 +86,7 @@ export class SearchAutocomplete {
// Only when user is logged in
if (gon.current_user_id) {
this.createAutocomplete();
this.createGlobalSearchInput();
}
this.bindEvents();
@ -117,7 +111,7 @@ export class SearchAutocomplete {
return (this.originalState = this.serializeState());
}
createAutocomplete() {
createGlobalSearchInput() {
return this.searchInput.glDropdown({
filterInputBlur: false,
filterable: true,
@ -149,116 +143,17 @@ export class SearchAutocomplete {
if (glDropdownInstance) {
glDropdownInstance.filter.options.callback(contents);
}
this.enableAutocomplete();
this.enableDropdown();
}
return;
}
// Prevent multiple ajax calls
if (this.loadingSuggestions) {
return;
}
const options = this.scopedSearchOptions(term);
this.loadingSuggestions = true;
callback(options);
return axios
.get(this.autocompletePath, {
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;
this.highlightFirstRow();
this.setScrollFade();
}
// Add option to proceed with the search for each
@ -343,7 +238,7 @@ export class SearchAutocomplete {
});
}
enableAutocomplete() {
enableDropdown() {
this.setScrollFade();
// No need to enable anything if user is not logged in
@ -360,7 +255,7 @@ export class SearchAutocomplete {
}
onSearchInputChange() {
this.enableAutocomplete();
this.enableDropdown();
}
onSearchInputKeyUp(e) {
@ -369,7 +264,7 @@ export class SearchAutocomplete {
this.restoreOriginalState();
break;
case KEYCODE.ENTER:
this.disableAutocomplete();
this.disableDropdown();
break;
default:
}
@ -422,7 +317,7 @@ export class SearchAutocomplete {
return results;
}
disableAutocomplete() {
disableDropdown() {
if (!this.searchInput.hasClass('js-autocomplete-disabled') && this.dropdown.hasClass('show')) {
this.searchInput.addClass('js-autocomplete-disabled');
this.dropdownToggle.dropdown('toggle');
@ -438,16 +333,8 @@ export class SearchAutocomplete {
onClick(item, $el, e) {
if (window.location.pathname.indexOf(item.url) !== -1) {
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');
this.disableAutocomplete();
this.disableDropdown();
return this.searchInput.val('').focus();
}
}
@ -456,20 +343,58 @@ export class SearchAutocomplete {
this.searchInput.data('glDropdown').highlightRowAtIndex(null, 0);
}
getAvatar(item) {
if (!Object.hasOwnProperty.call(item, 'avatar_url')) {
return 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 { label, id } = item;
const avatarUrl = item.avatar_url;
const avatar = avatarUrl
? `<img class="search-item-avatar" src="${avatarUrl}" />`
: `<div class="s16 avatar identicon ${getIdenticonBackgroundClass(id)}">${getIdenticonTitle(
escape(label),
)}</div>`;
const { issuesPath, mrPath, name, issuesDisabled } = options;
const baseItems = [];
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() {
@ -495,6 +420,6 @@ export class SearchAutocomplete {
}
}
export default function initSearchAutocomplete(opts) {
return new SearchAutocomplete(opts);
export default function initGlobalSearchInput(opts) {
return new GlobalSearchInput(opts);
}

View File

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

View File

@ -51,21 +51,6 @@ class SearchController < ApplicationController
render json: { count: count }
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
def preload_method

View File

@ -3,28 +3,6 @@
module SearchHelper
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)
return if collection.to_a.empty?
@ -95,91 +73,6 @@ module SearchHelper
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)
Sanitize.clean(str)
end

View File

@ -149,7 +149,9 @@ class Event < ApplicationRecord
def visible_to_user?(user = nil)
return false unless capability.present?
Ability.allowed?(user, capability, permission_object)
capability.all? do |rule|
Ability.allowed?(user, rule, permission_object)
end
end
def resource_parent
@ -361,34 +363,30 @@ class Event < ApplicationRecord
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
@capability ||= begin
if push_action? || commit_note?
:download_code
elsif membership_changed? || created_project_action?
:read_project
elsif issue? || issue_note?
:read_issue
elsif merge_request? || merge_request_note?
:read_merge_request
elsif personal_snippet_note? || project_snippet_note?
:read_snippet
elsif milestone?
:read_milestone
elsif wiki_page?
:read_wiki
elsif design_note? || design?
:read_design
end
end
capabilities.flat_map do |ability, syms|
if syms.any? { |sym| send(sym) } # rubocop: disable GitlabSecurity/PublicSend
[ability]
else
[]
end
end
end
end
def capabilities
{
download_code: %i[push_action? commit_note?],
read_project: %i[membership_changed? created_project_action?],
read_issue: %i[issue? issue_note?],
read_merge_request: %i[merge_request? merge_request_note?],
read_snippet: %i[personal_snippet_note? project_snippet_note?],
read_milestone: %i[milestone?],
read_wiki: %i[wiki_page?],
read_design: %i[design_note? design?]
}
end
# rubocop:enable Metrics/CyclomaticComplexity
# rubocop:enable Metrics/PerceivedComplexity
private

View File

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

View File

@ -150,7 +150,7 @@ module Projects
if @project.save
unless @project.gitlab_project_import?
create_services_from_active_templates(@project)
create_services_from_active_instances_or_templates(@project)
@project.create_labels
end
@ -175,15 +175,6 @@ module Projects
@project
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
service = @project.find_or_initialize_service(::PrometheusService.to_param)
@ -225,6 +216,15 @@ module Projects
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
@project_namespace ||= Namespace.find_by_id(@params[:namespace_id]) || current_user.namespace
end

View File

@ -2,7 +2,7 @@
= form_tag search_path, method: :get, class: 'form-inline' do |f|
.search-input-container
.search-input-wrap
.dropdown{ data: { url: search_autocomplete_path } }
.dropdown
= search_field_tag 'search', nil, placeholder: _('Search or jump to…'),
class: 'search-input dropdown-menu-toggle no-outline js-search-dashboard-options',
spellcheck: false,
@ -37,6 +37,3 @@
-# workaround for non-JS feature specs, see spec/support/helpers/search_helpers.rb
- if ENV['RAILS_ENV'] == 'test'
%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
- disaster_recovery
- dynamic_application_security_testing
- editor_extension
- epics
- error_tracking
- feature_flags
@ -52,6 +53,7 @@
- geo_replication
- git_lfs
- gitaly
- gitlab_docs
- gitlab_handbook
- gitter
- global_search
@ -82,6 +84,7 @@
- pages
- pki_management
- planning_analytics
- product_analytics
- quality_management
- release_evidence
- release_orchestration
@ -100,7 +103,6 @@
- source_code_management
- static_application_security_testing
- static_site_editor
- status_page
- subgroups
- templates
- 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::Middleware::RailsQueueDuration)
config.middleware.use(Gitlab::Metrics::RedisRackMiddleware)
config.middleware.use(Gitlab::Metrics::ElasticsearchRackMiddleware)
end
Sidekiq.configure_server do |config|

View File

@ -58,7 +58,6 @@ Rails.application.routes.draw do
# Search
get 'search' => 'search#show'
get 'search/autocomplete' => 'search#autocomplete', as: :search_autocomplete
get 'search/count' => 'search#count', as: :search_count
# 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_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_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 | |
| `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 | |

View File

@ -60,8 +60,10 @@ the following documents:
- [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).
If you're coming over from Jenkins, you can also check out our handy [reference](jenkins/index.md)
for converting your pipelines.
If you're migrating from another CI/CD tool, check out our handy references:
- [Migrating from CircleCI](migration/circleci.md)
- [Migrating from Jenkins](jenkins/index.md)
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)

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
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:**
To help you visualize what you're doing locally, there are
[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
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
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
def initialize(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
def call(env)
@ -13,7 +21,7 @@ module Gitlab
@app.call(env)
ensure
record_metrics(transaction) if transaction
record_metrics(transaction)
end
private
@ -23,14 +31,8 @@ module Gitlab
query_time = Gitlab::Instrumentation::Redis.query_time
request_count = Gitlab::Instrumentation::Redis.get_request_count
Gitlab::Metrics.counter(:http_redis_requests_total,
'Amount of calls to Redis servers during web requests',
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)
@requests_total_counter.increment(labels, request_count)
@requests_duration_histogram.observe(labels, query_time)
end
end
end

View File

@ -79,6 +79,7 @@ module Gitlab
config[:'gitaly-ruby'] = { dir: File.join(gitaly_dir, 'ruby') } if gitaly_ruby
config[:'gitlab-shell'] = { dir: Gitlab.config.gitlab_shell.path }
config[:bin_dir] = Gitlab.config.gitaly.client_path
config[:gitlab] = { url: Gitlab.config.gitlab.url }
TomlRB.dump(config)
end
@ -97,7 +98,8 @@ module Gitlab
def configuration_toml(gitaly_dir, storage_paths)
nodes = [{ storage: 'default', address: "unix:#{gitaly_dir}/gitaly.socket", primary: true, token: 'secret' }]
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?
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}'"
msgstr ""
msgid "API Help"
msgstr ""
msgid "API Token"
msgstr ""
@ -1441,9 +1438,6 @@ msgstr ""
msgid "Admin Overview"
msgstr ""
msgid "Admin Section"
msgstr ""
msgid "Admin mode already enabled"
msgstr ""
@ -7487,9 +7481,6 @@ msgstr ""
msgid "DesignManagement|%{filename} did not change."
msgstr ""
msgid "DesignManagement|Add designs"
msgstr ""
msgid "DesignManagement|Adding a design with the same filename replaces the file in a new version."
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."
msgstr ""
msgid "DesignManagement|Upload designs"
msgstr ""
msgid "DesignManagement|Upload skipped."
msgstr ""
@ -13336,9 +13330,6 @@ msgstr ""
msgid "Markdown"
msgstr ""
msgid "Markdown Help"
msgstr ""
msgid "Markdown enabled"
msgstr ""
@ -15654,9 +15645,6 @@ msgstr ""
msgid "Permissions"
msgstr ""
msgid "Permissions Help"
msgstr ""
msgid "Permissions, LFS, 2FA"
msgstr ""
@ -17685,9 +17673,6 @@ msgstr ""
msgid "Public - The project can be accessed without any authentication."
msgstr ""
msgid "Public Access Help"
msgstr ""
msgid "Public deploy keys (%{deploy_keys_count})"
msgstr ""
@ -17817,9 +17802,6 @@ msgstr ""
msgid "README"
msgstr ""
msgid "Rake Tasks Help"
msgstr ""
msgid "Raw blob request rate limit per minute"
msgstr ""
@ -18861,9 +18843,6 @@ msgstr ""
msgid "SSH Keys"
msgstr ""
msgid "SSH Keys Help"
msgstr ""
msgid "SSH host key fingerprints"
msgstr ""
@ -21343,9 +21322,6 @@ msgstr ""
msgid "System Hooks"
msgstr ""
msgid "System Hooks Help"
msgstr ""
msgid "System Info"
msgstr ""
@ -23975,9 +23951,6 @@ msgstr ""
msgid "User restrictions"
msgstr ""
msgid "User settings"
msgstr ""
msgid "User was successfully created."
msgstr ""
@ -24789,9 +24762,6 @@ msgstr ""
msgid "Webhooks"
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."
msgstr ""
@ -25058,9 +25028,6 @@ msgstr ""
msgid "Work in progress Limit"
msgstr ""
msgid "Workflow Help"
msgstr ""
msgid "Write"
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
abort 'gitaly build failed' unless system(env, 'make', chdir: tmp_tests_gitaly_dir)
ensure_gitlab_shell_secret!
check_gitaly_config!
# Starting gitaly further validates its configuration

View File

@ -4,6 +4,7 @@
# Please be careful when modifying this file. Your changes must work
# both for local development rspec runs, and in CI.
require 'securerandom'
require 'socket'
module GitalyTest
@ -11,10 +12,22 @@ module GitalyTest
File.expand_path('../tmp/tests/gitaly', __dir__)
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
File.join(tmp_tests_gitaly_dir, 'ruby', 'Gemfile')
end
def gitlab_shell_secret_file
File.join(tmp_tests_gitlab_shell_dir, '.gitlab_shell_secret')
end
def env
env_hash = {
'HOME' => File.expand_path('tmp/tests'),
@ -70,6 +83,20 @@ module GitalyTest
pid
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!
puts "Checking gitaly-ruby Gemfile..."

View File

@ -211,9 +211,4 @@ describe SearchController do
end.to raise_error(ActionController::ParameterMissing)
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

View File

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

View File

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

View File

@ -8,99 +8,6 @@ describe SearchHelper do
str
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
using RSpec::Parameterized::TableSyntax

View File

@ -2,10 +2,10 @@
import $ from 'jquery';
import '~/gl_dropdown';
import initSearchAutocomplete from '~/search_autocomplete';
import initGlobalSearchInput from '~/global_search_input';
import '~/lib/utils/common_utils';
describe('Search autocomplete dropdown', () => {
describe('Global search input dropdown', () => {
let widget = null;
const userName = 'root';
@ -112,15 +112,15 @@ describe('Search autocomplete dropdown', () => {
expect(list.find(mrsIHaveCreatedLink).text()).toBe("Merge requests I've created");
};
preloadFixtures('static/search_autocomplete.html');
preloadFixtures('static/global_search_input.html');
beforeEach(function() {
loadFixtures('static/search_autocomplete.html');
loadFixtures('static/global_search_input.html');
window.gon = {};
window.gon.current_user_id = userId;
window.gon.current_username = userName;
return (widget = initSearchAutocomplete());
return (widget = initGlobalSearchInput());
});
afterEach(function() {
@ -189,25 +189,25 @@ describe('Search autocomplete dropdown', () => {
expect(submitSpy).not.toHaveBeenTriggered();
});
describe('disableAutocomplete', function() {
describe('disableDropdown', function() {
beforeEach(function() {
widget.enableAutocomplete();
widget.enableDropdown();
});
it('should close the Dropdown', function() {
const toggleSpy = spyOn(widget.dropdownToggle, 'dropdown');
widget.dropdown.addClass('show');
widget.disableAutocomplete();
widget.disableDropdown();
expect(toggleSpy).toHaveBeenCalledWith('toggle');
});
});
describe('enableAutocomplete', function() {
describe('enableDropdown', function() {
it('should open the Dropdown', function() {
const toggleSpy = spyOn(widget.dropdownToggle, 'dropdown');
widget.enableAutocomplete();
widget.enableDropdown();
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
describe '#call' do
context 'when metrics are disabled' do
before do
allow(Gitlab::Metrics).to receive(:current_transaction).and_return(nil)
end
let(:counter) { double(Prometheus::Client::Counter, increment: nil) }
let(:histogram) { double(Prometheus::Client::Histogram, observe: nil) }
let(:redis_query_time) { 0.1 }
let(:redis_requests_count) { 2 }
it 'calls the app' do
expect(middleware.call(env)).to eq('wub wub')
end
before do
allow(Gitlab::Instrumentation::Redis).to receive(:query_time) { redis_query_time }
allow(Gitlab::Instrumentation::Redis).to receive(:get_request_count) { redis_requests_count }
it 'does not record metrics' do
expect(Gitlab::Metrics).not_to receive(:counter)
expect(Gitlab::Metrics).not_to receive(:histogram)
allow(Gitlab::Metrics).to receive(:counter)
.with(:http_redis_requests_total,
an_instance_of(String),
Gitlab::Metrics::Transaction::BASE_LABELS)
.and_return(counter)
middleware.call(env)
end
allow(Gitlab::Metrics).to receive(:histogram)
.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
context 'when metrics are enabled' do
let(:counter) { double(Prometheus::Client::Counter, increment: nil) }
let(:histogram) { double(Prometheus::Client::Histogram, observe: nil) }
let(:redis_query_time) { 0.1 }
let(:redis_requests_count) { 2 }
it 'calls the app' do
expect(middleware.call(env)).to eq('wub wub')
end
before do
allow(Gitlab::Instrumentation::Redis).to receive(:query_time) { redis_query_time }
allow(Gitlab::Instrumentation::Redis).to receive(:get_request_count) { redis_requests_count }
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)
allow(Gitlab::Metrics).to receive(:counter)
.with(:http_redis_requests_total,
an_instance_of(String),
Gitlab::Metrics::Transaction::BASE_LABELS)
.and_return(counter)
middleware.call(env)
end
allow(Gitlab::Metrics).to receive(:histogram)
.with(:http_redis_requests_duration_seconds,
an_instance_of(String),
Gitlab::Metrics::Transaction::BASE_LABELS,
Gitlab::Instrumentation::Redis::QUERY_TIME_BUCKETS)
.and_return(histogram)
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(Gitlab::Metrics).to receive(:current_transaction).and_return(transaction)
end
allow(app).to receive(:call).with(env).and_raise(StandardError)
it 'calls the app' do
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
expect { middleware.call(env) }.to raise_error(StandardError)
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
context 'when failed to save' do
context 'when failed to save merge request' do
before do
allow(merge_request).to receive(:save) { false }
allow(merge_request).to receive(:save!) { raise ActiveRecord::RecordInvalid.new }
end
it 'does not yield block' do
@ -94,6 +94,39 @@ describe AutoMerge::BaseService do
it 'returns failed' do
is_expected.to eq(:failed)
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
@ -162,7 +195,7 @@ describe AutoMerge::BaseService do
context 'when failed to save' do
before do
allow(merge_request).to receive(:save) { false }
allow(merge_request).to receive(:save!) { raise ActiveRecord::RecordInvalid.new }
end
it 'does not yield block' do
@ -178,9 +211,9 @@ describe AutoMerge::BaseService do
it_behaves_like 'Canceled or Dropped'
context 'when failed to save' do
context 'when failed to save merge request' do
before do
allow(merge_request).to receive(:save) { false }
allow(merge_request).to receive(:save!) { raise ActiveRecord::RecordInvalid.new }
end
it 'returns error status' do
@ -188,6 +221,33 @@ describe AutoMerge::BaseService do
expect(subject[:message]).to eq("Can't cancel the automatic merge")
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
describe '#abort' do
@ -200,7 +260,7 @@ describe AutoMerge::BaseService do
context 'when failed to save' do
before do
allow(merge_request).to receive(:save) { false }
allow(merge_request).to receive(:save!) { raise ActiveRecord::RecordInvalid.new }
end
it 'returns error status' do
@ -208,5 +268,32 @@ describe AutoMerge::BaseService do
expect(subject[:message]).to eq("Can't abort the automatic merge")
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

View File

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