Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-09-04 15:08:46 +00:00
parent 6724a6ee6b
commit adf76f8f1d
71 changed files with 1039 additions and 309 deletions

View file

@ -729,8 +729,6 @@ Rails/SaveBang:
- 'ee/spec/models/merge_train_spec.rb'
- 'spec/models/packages/package_spec.rb'
- 'ee/spec/models/project_ci_cd_setting_spec.rb'
- 'ee/spec/models/project_services/github_service_spec.rb'
- 'ee/spec/models/project_services/jenkins_service_spec.rb'
- 'ee/spec/models/project_spec.rb'
- 'ee/spec/models/protected_environment_spec.rb'
- 'ee/spec/models/repository_spec.rb'
@ -1113,12 +1111,6 @@ Rails/SaveBang:
- 'spec/models/pages_domain_spec.rb'
- 'spec/models/project_auto_devops_spec.rb'
- 'spec/models/project_feature_spec.rb'
- 'spec/models/project_services/bamboo_service_spec.rb'
- 'spec/models/project_services/buildkite_service_spec.rb'
- 'spec/models/project_services/jira_service_spec.rb'
- 'spec/models/project_services/packagist_service_spec.rb'
- 'spec/models/project_services/pipelines_email_service_spec.rb'
- 'spec/models/project_services/teamcity_service_spec.rb'
- 'spec/models/project_spec.rb'
- 'spec/models/project_team_spec.rb'
- 'spec/models/protectable_dropdown_spec.rb'

View file

@ -1,35 +0,0 @@
import $ from 'jquery';
export default class AjaxLoadingSpinner {
static init() {
const $elements = $('.js-ajax-loading-spinner');
$elements.on('ajax:beforeSend', AjaxLoadingSpinner.ajaxBeforeSend);
$elements.on('ajax:complete', AjaxLoadingSpinner.ajaxComplete);
}
static ajaxBeforeSend(e) {
e.target.setAttribute('disabled', '');
const iconElement = e.target.querySelector('i');
// get first fa- icon
const originalIcon = iconElement.className.match(/(fa-)([^\s]+)/g)[0];
iconElement.dataset.icon = originalIcon;
AjaxLoadingSpinner.toggleLoadingIcon(iconElement);
$(e.target).off('ajax:beforeSend', AjaxLoadingSpinner.ajaxBeforeSend);
}
static ajaxComplete(e) {
e.target.removeAttribute('disabled');
const iconElement = e.target.querySelector('i');
AjaxLoadingSpinner.toggleLoadingIcon(iconElement);
$(e.target).off('ajax:complete', AjaxLoadingSpinner.ajaxComplete);
}
static toggleLoadingIcon(iconElement) {
const { classList } = iconElement;
classList.toggle(iconElement.dataset.icon);
classList.toggle('gl-spinner');
classList.toggle('gl-spinner-orange');
classList.toggle('gl-spinner-sm');
}
}

View file

@ -0,0 +1,31 @@
import $ from 'jquery';
export default class AjaxLoadingSpinner {
static init() {
const $elements = $('.js-ajax-loading-spinner');
$elements.on('ajax:beforeSend', AjaxLoadingSpinner.ajaxBeforeSend);
}
static ajaxBeforeSend(e) {
const button = e.target;
const newButton = document.createElement('button');
newButton.classList.add('btn', 'btn-default', 'disabled', 'gl-button');
newButton.setAttribute('disabled', 'disabled');
const spinner = document.createElement('span');
spinner.classList.add('align-text-bottom', 'gl-spinner', 'gl-spinner-sm', 'gl-spinner-orange');
newButton.appendChild(spinner);
button.classList.add('hidden');
button.parentNode.insertBefore(newButton, button.nextSibling);
$(button).one('ajax:error', () => {
newButton.remove();
button.classList.remove('hidden');
});
$(button).one('ajax:success', () => {
$(button).off('ajax:beforeSend', AjaxLoadingSpinner.ajaxBeforeSend);
});
}
}

View file

@ -1,7 +1,7 @@
<script>
import { GlLink, GlSprintf } from '@gitlab/ui';
import { s__ } from '~/locale';
import DetailsRow from '~/registry/shared/components/details_row.vue';
import DetailsRow from '~/vue_shared/components/registry/details_row.vue';
import { generateConanRecipe } from '../utils';
import { PackageType } from '../../shared/constants';

View file

@ -2,7 +2,7 @@
import { GlLink, GlSprintf } from '@gitlab/ui';
import { s__ } from '~/locale';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import HistoryElement from './history_element.vue';
import HistoryItem from '~/vue_shared/components/registry/history_item.vue';
export default {
name: 'PackageHistory',
@ -16,7 +16,7 @@ export default {
components: {
GlLink,
GlSprintf,
HistoryElement,
HistoryItem,
TimeAgoTooltip,
},
props: {
@ -46,7 +46,7 @@ export default {
<div class="issuable-discussion">
<h3 class="gl-font-lg" data-testid="title">{{ __('History') }}</h3>
<ul class="timeline main-notes-list notes gl-mb-4" data-testid="timeline">
<history-element icon="clock" data-testid="created-on">
<history-item icon="clock" data-testid="created-on">
<gl-sprintf :message="$options.i18n.createdOn">
<template #name>
<strong>{{ packageEntity.name }}</strong>
@ -58,8 +58,8 @@ export default {
<time-ago-tooltip :time="packageEntity.created_at" />
</template>
</gl-sprintf>
</history-element>
<history-element icon="pencil" data-testid="updated-at">
</history-item>
<history-item icon="pencil" data-testid="updated-at">
<gl-sprintf :message="$options.i18n.updatedAtText">
<template #name>
<strong>{{ packageEntity.name }}</strong>
@ -71,9 +71,9 @@ export default {
<time-ago-tooltip :time="packageEntity.updated_at" />
</template>
</gl-sprintf>
</history-element>
</history-item>
<template v-if="packagePipeline">
<history-element icon="commit" data-testid="commit">
<history-item icon="commit" data-testid="commit">
<gl-sprintf :message="$options.i18n.commitText">
<template #link>
<gl-link :href="packagePipeline.project.commit_url">{{
@ -84,8 +84,8 @@ export default {
<strong>{{ packagePipeline.ref }}</strong>
</template>
</gl-sprintf>
</history-element>
<history-element icon="pipeline" data-testid="pipeline">
</history-item>
<history-item icon="pipeline" data-testid="pipeline">
<gl-sprintf :message="$options.i18n.pipelineText">
<template #link>
<gl-link :href="packagePipeline.project.pipeline_url"
@ -97,9 +97,9 @@ export default {
</template>
<template #author>{{ packagePipeline.user.name }}</template>
</gl-sprintf>
</history-element>
</history-item>
</template>
<history-element icon="package" data-testid="published">
<history-item icon="package" data-testid="published">
<gl-sprintf :message="$options.i18n.publishText">
<template #project>
<strong>{{ projectName }}</strong>
@ -108,7 +108,7 @@ export default {
<time-ago-tooltip :time="packageEntity.created_at" />
</template>
</gl-sprintf>
</history-element>
</history-item>
</ul>
</div>
</template>

View file

@ -1,4 +1,4 @@
import AjaxLoadingSpinner from '~/ajax_loading_spinner';
import AjaxLoadingSpinner from '~/branches/ajax_loading_spinner';
import DeleteModal from '~/branches/branches_delete_modal';
import initDiverganceGraph from '~/branches/divergence_graph';

View file

@ -7,7 +7,7 @@ import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import { formatDate } from '~/lib/utils/datetime_utility';
import ListItem from '~/vue_shared/components/registry/list_item.vue';
import DeleteButton from '../delete_button.vue';
import DetailsRow from '~/registry/shared/components/details_row.vue';
import DetailsRow from '~/vue_shared/components/registry/details_row.vue';
import {
REMOVE_TAG_BUTTON_TITLE,
DIGEST_LABEL,

View file

@ -1,5 +1,6 @@
<script>
/* eslint-disable vue/no-v-html */
import { GlLoadingIcon } from '@gitlab/ui';
import { sprintf, s__ } from '~/locale';
import statusCodes from '~/lib/utils/http_status';
import { bytesToMiB } from '~/lib/utils/number_utils';
@ -11,6 +12,7 @@ export default {
name: 'MemoryUsage',
components: {
MemoryGraph,
GlLoadingIcon,
},
props: {
metricsUrl: {
@ -156,8 +158,9 @@ export default {
<template>
<div class="mr-info-list clearfix mr-memory-usage js-mr-memory-usage">
<p v-if="shouldShowLoading" class="usage-info js-usage-info usage-info-loading">
<i class="fa fa-spinner fa-spin usage-info-load-spinner" aria-hidden="true"> </i
>{{ s__('mrWidget|Loading deployment statistics') }}
<gl-loading-icon class="usage-info-load-spinner" />{{
s__('mrWidget|Loading deployment statistics')
}}
</p>
<p
v-if="shouldShowMemoryGraph"

View file

@ -3,12 +3,11 @@ import { GlIcon } from '@gitlab/ui';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
export default {
name: 'HistoryElement',
name: 'HistoryItem',
components: {
GlIcon,
TimelineEntryItem,
},
props: {
icon: {
type: String,
@ -29,7 +28,9 @@ export default {
<slot></slot>
</span>
</div>
<div class="note-body"></div>
<div class="note-body">
<slot name="body"></slot>
</div>
</div>
</timeline-entry-item>
</template>

View file

@ -919,12 +919,12 @@
}
.issuable-todo-btn {
.fa-spinner {
.gl-spinner {
display: none;
}
&.is-loading {
.fa-spinner {
.gl-spinner {
display: inline-block;
}

View file

@ -55,8 +55,7 @@ module SendFileUpload
def image_scaling_request?(file_upload)
avatar_safe_for_scaling?(file_upload) &&
scaling_allowed_by_feature_flags?(file_upload) &&
valid_image_scaling_width? &&
current_user
valid_image_scaling_width?
end
def avatar_safe_for_scaling?(file_upload)

View file

@ -4,7 +4,7 @@ module IssuesHelper
def issue_css_classes(issue)
classes = ["issue"]
classes << "closed" if issue.closed?
classes << "today" if issue.today?
classes << "today" if issue.new?
classes << "user-can-drag" if @sort == 'relative_position'
classes.join(' ')
end

View file

@ -385,8 +385,12 @@ module Issuable
Date.today == created_at.to_date
end
def created_hours_ago
(Time.now.utc.to_i - created_at.utc.to_i) / 3600
end
def new?
today? && created_at == updated_at
created_hours_ago < 24
end
def open?

View file

@ -17,8 +17,6 @@ module Issues
Issues::CloseService
end
private
NO_REBALANCING_NEEDED = ((RelativePositioning::MIN_POSITION * 0.9999)..(RelativePositioning::MAX_POSITION * 0.9999)).freeze
def rebalance_if_needed(issue)
@ -32,6 +30,8 @@ module Issues
IssueRebalancingWorker.perform_async(nil, issue.project_id)
end
private
def create_assignee_note(issue, old_assignees)
SystemNoteService.change_issuable_assignees(
issue, issue.project, current_user, old_assignees)

View file

@ -16,12 +16,12 @@ module Issues
def before_create(issue)
spam_check(issue, current_user, action: :create)
issue.move_to_end
# current_user (defined in BaseService) is not available within run_after_commit block
user = current_user
issue.run_after_commit do
NewIssueWorker.perform_async(issue.id, user.id)
IssuePlacementWorker.perform_async(issue.id)
end
end
@ -30,7 +30,6 @@ module Issues
user_agent_detail_service.create
resolve_discussions_with_issue(issuable)
delete_milestone_total_issue_counter_cache(issuable.milestone)
rebalance_if_needed(issuable)
super
end

View file

@ -50,25 +50,25 @@
- if can?(current_user, :push_code, @project)
- if branch.name == @project.repository.root_ref
%button{ class: "btn btn-remove remove-row js-ajax-loading-spinner has-tooltip disabled",
%button{ class: "btn btn-remove remove-row has-tooltip disabled",
disabled: true,
title: s_('Branches|The default branch cannot be deleted') }
= icon("trash-o")
= sprite_icon("remove")
- elsif protected_branch?(@project, branch)
- if can?(current_user, :push_to_delete_protected_branch, @project)
%button{ class: "btn btn-remove remove-row js-ajax-loading-spinner has-tooltip",
%button{ class: "btn btn-remove remove-row has-tooltip",
title: s_('Branches|Delete protected branch'),
data: { toggle: "modal",
target: "#modal-delete-branch",
delete_path: project_branch_path(@project, branch.name),
branch_name: branch.name,
is_merged: ("true" if merged) } }
= icon("trash-o")
= sprite_icon("remove")
- else
%button{ class: "btn btn-remove remove-row js-ajax-loading-spinner has-tooltip disabled",
%button{ class: "btn btn-remove remove-row has-tooltip disabled",
disabled: true,
title: s_('Branches|Only a project maintainer or owner can delete a protected branch') }
= icon("trash-o")
= sprite_icon("remove")
- else
= link_to project_branch_path(@project, branch.name),
class: "btn btn-remove remove-row qa-remove-btn js-ajax-loading-spinner has-tooltip",
@ -77,4 +77,4 @@
data: { confirm: s_("Branches|Deleting the '%{branch_name}' branch cannot be undone. Are you sure?") % { branch_name: branch.name } },
remote: true,
'aria-label' => s_('Branches|Delete branch') do
= icon("trash-o")
= sprite_icon("remove")

View file

@ -63,7 +63,7 @@
// Fallback while content is loading
.title.hide-collapsed
= _('Time tracking')
= icon('spinner spin', 'aria-hidden': 'true')
= loading_icon
- if issuable_sidebar.has_key?(:due_date)
.block.due_date
.sidebar-collapsed-icon.has-tooltip{ data: { placement: 'left', container: 'body', html: 'true', boundary: 'viewport' }, title: sidebar_due_date_tooltip_label(issuable_sidebar[:due_date]) }

View file

@ -12,4 +12,4 @@
data: todo_button_data }
%span.issuable-todo-inner.js-issuable-todo-inner<
= is_collapsed ? button_icon : button_title
= icon('spin spinner', 'aria-hidden': 'true')
= loading_icon

View file

@ -1452,6 +1452,14 @@
:weight: 1
:idempotent:
:tags: []
- :name: issue_placement
:feature_category: :issue_tracking
:has_external_dependencies:
:urgency: :high
:resource_boundary: :cpu
:weight: 2
:idempotent: true
:tags: []
- :name: issue_rebalancing
:feature_category: :issue_tracking
:has_external_dependencies:

View file

@ -0,0 +1,36 @@
# frozen_string_literal: true
class IssuePlacementWorker
include ApplicationWorker
idempotent!
feature_category :issue_tracking
urgency :high
worker_resource_boundary :cpu
weight 2
# Move at most the most recent 100 issues
QUERY_LIMIT = 100
# rubocop: disable CodeReuse/ActiveRecord
def perform(issue_id)
issue = Issue.id_in(issue_id).first
return unless issue
# Move the most recent 100 unpositioned items to the end.
# This is to deal with out-of-order execution of the worker,
# while preserving creation order.
to_place = Issue
.relative_positioning_query_base(issue)
.where(relative_position: nil)
.order({ created_at: :desc }, { id: :desc })
.limit(QUERY_LIMIT)
Issue.move_nulls_to_end(to_place.to_a.reverse)
Issues::BaseService.new(nil).rebalance_if_needed(to_place.max_by(&:relative_position))
rescue RelativePositioning::NoSpaceLeft => e
Gitlab::ErrorTracking.log_exception(e, issue_id: issue_id)
IssueRebalancingWorker.perform_async(nil, issue.project_id)
end
# rubocop: enable CodeReuse/ActiveRecord
end

View file

@ -12,8 +12,8 @@ class NewIssueWorker # rubocop:disable Scalability/IdempotentWorker
def perform(issue_id, user_id)
return unless objects_found?(issue_id, user_id)
EventCreateService.new.open_issue(issuable, user)
NotificationService.new.new_issue(issuable, user)
::EventCreateService.new.open_issue(issuable, user)
::NotificationService.new.new_issue(issuable, user)
issuable.create_cross_references!(user)
end

View file

@ -0,0 +1,5 @@
---
title: Migrate '.fa-spinner' to '.spinner' for 'app/assets/javascripts/vue_merge_request_widget/components/deployment/memory_usage.vue'
merge_request: 41142
author: Gilang Gumilar
type: changed

View file

@ -0,0 +1,5 @@
---
title: Migrate '.fa-spinner' to '.spinner' for 'app/views/shared/issuable'
merge_request: 41132
author: Gilang Gumilar
type: changed

View file

@ -0,0 +1,5 @@
---
title: Adds monthly package data to usage ping
merge_request: 40452
author:
type: added

View file

@ -0,0 +1,5 @@
---
title: Ensure issue creation is not blocked by positioning
merge_request: 41313
author:
type: fixed

View file

@ -0,0 +1,5 @@
---
title: Change logic behind new issues highlight
merge_request: 41150
author:
type: changed

View file

@ -0,0 +1,5 @@
---
title: Change icon for branch delete button
merge_request: 39968
author:
type: changed

View file

@ -0,0 +1,5 @@
---
title: Fix Rails/SaveBang offenses for *spec/models/project_services*
merge_request: 41320
author: Rajendra Kadam
type: fixed

View file

@ -1,5 +1,5 @@
---
name: security-on-demand-scans-site-validation
name: security_on_demand_scans_site_validation
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/40685
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/241815
group: group::dynamic analysis

View file

@ -7,7 +7,9 @@ require "carrierwave/storage/fog"
#
# This patch also incorporates
# https://github.com/carrierwaveuploader/carrierwave/pull/2375 to
# provide Azure support. This is already in CarrierWave v2.1.x, but
# provide Azure support
# and https://github.com/carrierwaveuploader/carrierwave/pull/2397 to
# support custom expire_at. This is already in CarrierWave v2.1.x, but
# upgrading this gem is a significant task:
# https://gitlab.com/gitlab-org/gitlab/-/issues/216067
module CarrierWave
@ -28,7 +30,7 @@ module CarrierWave
# avoid a get by using local references
local_directory = connection.directories.new(key: @uploader.fog_directory)
local_file = local_directory.files.new(key: path)
expire_at = ::Fog::Time.now + @uploader.fog_authenticated_url_expiration
expire_at = options[:expire_at] || ::Fog::Time.now + @uploader.fog_authenticated_url_expiration
case @uploader.fog_credentials[:provider]
when 'AWS', 'Google'
# Older versions of fog-google do not support options as a parameter

View file

@ -138,6 +138,8 @@
- 2
- - irker
- 1
- - issue_placement
- 2
- - issue_rebalancing
- 1
- - jira_connect

View file

@ -0,0 +1,19 @@
# frozen_string_literal: true
class AddIdCreatedAtIndexToPackages < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
INDEX_NAME = 'index_packages_packages_on_id_and_created_at'
def up
add_concurrent_index :packages_packages, [:id, :created_at], name: INDEX_NAME
end
def down
remove_concurrent_index_by_name(:packages_packages, INDEX_NAME)
end
end

View file

@ -0,0 +1 @@
eb13fb285ac9af83bbc66397a5352a824575ad4af93178b98fbfc1be2e11ce8b

View file

@ -20463,6 +20463,8 @@ CREATE INDEX index_packages_package_files_on_package_id_and_file_name ON public.
CREATE INDEX index_packages_packages_on_creator_id ON public.packages_packages USING btree (creator_id);
CREATE INDEX index_packages_packages_on_id_and_created_at ON public.packages_packages USING btree (id, created_at);
CREATE INDEX index_packages_packages_on_name_trigram ON public.packages_packages USING gin (name public.gin_trgm_ops);
CREATE INDEX index_packages_packages_on_project_id_and_created_at ON public.packages_packages USING btree (project_id, created_at);

View file

@ -6016,7 +6016,7 @@ type GeoNode {
name: String
"""
Package file registries of the GeoNode. Available only when feature flag `geo_self_service_framework` is enabled
Package file registries of the GeoNode. Available only when feature flag `geo_package_file_replication` is enabled
"""
packageFileRegistries(
"""
@ -6095,6 +6095,37 @@ type GeoNode {
"""
syncObjectStorage: Boolean
"""
Find terraform state registries on this Geo node. Available only when feature
flag `geo_terraform_state_replication` is enabled
"""
terraformStateRegistries(
"""
Returns the elements in the list that come after the specified cursor.
"""
after: String
"""
Returns the elements in the list that come before the specified cursor.
"""
before: String
"""
Returns the first _n_ elements from the list.
"""
first: Int
"""
Filters registries by their ID
"""
ids: [ID!]
"""
Returns the last _n_ elements from the list.
"""
last: Int
): TerraformStateRegistryConnection
"""
The user-facing URL for this Geo node
"""
@ -15712,6 +15743,86 @@ type TaskCompletionStatus {
count: Int!
}
"""
Represents the sync and verification state of a terraform state
"""
type TerraformStateRegistry {
"""
Timestamp when the TerraformStateRegistry was created
"""
createdAt: Time
"""
ID of the TerraformStateRegistry
"""
id: ID!
"""
Error message during sync of the TerraformStateRegistry
"""
lastSyncFailure: String
"""
Timestamp of the most recent successful sync of the TerraformStateRegistry
"""
lastSyncedAt: Time
"""
Timestamp after which the TerraformStateRegistry should be resynced
"""
retryAt: Time
"""
Number of consecutive failed sync attempts of the TerraformStateRegistry
"""
retryCount: Int
"""
Sync state of the TerraformStateRegistry
"""
state: RegistryState
"""
ID of the TerraformState
"""
terraformStateId: ID!
}
"""
The connection type for TerraformStateRegistry.
"""
type TerraformStateRegistryConnection {
"""
A list of edges.
"""
edges: [TerraformStateRegistryEdge]
"""
A list of nodes.
"""
nodes: [TerraformStateRegistry]
"""
Information to aid in pagination.
"""
pageInfo: PageInfo!
}
"""
An edge in a connection.
"""
type TerraformStateRegistryEdge {
"""
A cursor for use in pagination.
"""
cursor: String!
"""
The item at the end of the edge.
"""
node: TerraformStateRegistry
}
"""
Represents a requirement test report.
"""

View file

@ -16819,7 +16819,7 @@
},
{
"name": "packageFileRegistries",
"description": "Package file registries of the GeoNode. Available only when feature flag `geo_self_service_framework` is enabled",
"description": "Package file registries of the GeoNode. Available only when feature flag `geo_package_file_replication` is enabled",
"args": [
{
"name": "ids",
@ -17019,6 +17019,77 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "terraformStateRegistries",
"description": "Find terraform state registries on this Geo node. Available only when feature flag `geo_terraform_state_replication` is enabled",
"args": [
{
"name": "ids",
"description": "Filters registries by their ID",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
}
},
"defaultValue": null
},
{
"name": "after",
"description": "Returns the elements in the list that come after the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "before",
"description": "Returns the elements in the list that come before the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "first",
"description": "Returns the first _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
},
{
"name": "last",
"description": "Returns the last _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "TerraformStateRegistryConnection",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "url",
"description": "The user-facing URL for this Geo node",
@ -46322,6 +46393,251 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "TerraformStateRegistry",
"description": "Represents the sync and verification state of a terraform state",
"fields": [
{
"name": "createdAt",
"description": "Timestamp when the TerraformStateRegistry was created",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "Time",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "id",
"description": "ID of the TerraformStateRegistry",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "lastSyncFailure",
"description": "Error message during sync of the TerraformStateRegistry",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "lastSyncedAt",
"description": "Timestamp of the most recent successful sync of the TerraformStateRegistry",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "Time",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "retryAt",
"description": "Timestamp after which the TerraformStateRegistry should be resynced",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "Time",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "retryCount",
"description": "Number of consecutive failed sync attempts of the TerraformStateRegistry",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "state",
"description": "Sync state of the TerraformStateRegistry",
"args": [
],
"type": {
"kind": "ENUM",
"name": "RegistryState",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "terraformStateId",
"description": "ID of the TerraformState",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "TerraformStateRegistryConnection",
"description": "The connection type for TerraformStateRegistry.",
"fields": [
{
"name": "edges",
"description": "A list of edges.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "TerraformStateRegistryEdge",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "nodes",
"description": "A list of nodes.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "TerraformStateRegistry",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "pageInfo",
"description": "Information to aid in pagination.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "PageInfo",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "TerraformStateRegistryEdge",
"description": "An edge in a connection.",
"fields": [
{
"name": "cursor",
"description": "A cursor for use in pagination.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "node",
"description": "The item at the end of the edge.",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "TerraformStateRegistry",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "TestReport",

View file

@ -2317,6 +2317,21 @@ Completion status of tasks
| `completedCount` | Int! | Number of completed tasks |
| `count` | Int! | Number of total tasks |
## TerraformStateRegistry
Represents the sync and verification state of a terraform state
| Name | Type | Description |
| --- | ---- | ---------- |
| `createdAt` | Time | Timestamp when the TerraformStateRegistry was created |
| `id` | ID! | ID of the TerraformStateRegistry |
| `lastSyncFailure` | String | Error message during sync of the TerraformStateRegistry |
| `lastSyncedAt` | Time | Timestamp of the most recent successful sync of the TerraformStateRegistry |
| `retryAt` | Time | Timestamp after which the TerraformStateRegistry should be resynced |
| `retryCount` | Int | Number of consecutive failed sync attempts of the TerraformStateRegistry |
| `state` | RegistryState | Sync state of the TerraformStateRegistry |
| `terraformStateId` | ID! | ID of the TerraformState |
## TestReport
Represents a requirement test report.

View file

@ -566,7 +566,7 @@ the Admin Area UI, and Prometheus!
null: true,
resolver: ::Resolvers::Geo::WidgetRegistriesResolver,
description: 'Find widget registries on this Geo node',
feature_flag: :geo_self_service_framework
feature_flag: :geo_widget_replication
```
1. Add the new `widget_registries` field name to the `expected_fields` array in

View file

@ -138,7 +138,90 @@ const label = __('Subscribe');
```
In order to test JavaScript translations you have to change the GitLab
localization to other language than English and you have to generate JSON files
localization to another language than English and you have to generate JSON files
using `bin/rake gettext:po_to_json` or `bin/rake gettext:compile`.
### Vue files
In Vue files we make both the `__()` (double underscore parenthesis) function and the `s__()` (namespaced double underscore parenthesis) function available that you can import from the `~/locale` file. For instance:
```javascript
import { __, s__ } from '~/locale';
const label = __('Subscribe');
const nameSpacedlabel = __('Plan|Subscribe');
```
For the static text strings we suggest two patterns for using these translations in Vue files:
- External constants file:
```javascript
javascripts
└───alert_settings
│ │ constants.js
│ └───components
│ │ alert_settings_form.vue
// constants.js
import { s__ } from '~/locale';
/* Integration constants */
export const I18N_ALERT_SETTINGS_FORM = {
saveBtnLabel: __('Save changes'),
};
// alert_settings_form.vue
import {
I18N_ALERT_SETTINGS_FORM,
} from '../constants';
<script>
export default {
i18n: {
I18N_ALERT_SETTINGS_FORM,
}
}
</script>
<template>
<gl-button
ref="submitBtn"
variant="success"
type="submit"
>
{{ $options.i18n.I18N_ALERT_SETTINGS_FORM }}
</gl-button>
</template>
```
When possible, you should opt for this pattern, as this allows you to import these strings directly into your component specs for re-use during testing.
- Internal component `$options` object `:
```javascript
<script>
export default {
i18n: {
buttonLabel: s__('Plan|Button Label')
}
},
</script>
<template>
<gl-button :aria-label="$options.i18n.buttonLabel">
{{ $options.i18n.buttonLabel }}
</gl-button>
</template>
```
In order to visually test the Vue translations you have to change the GitLab
localization to another language than English and you have to generate JSON files
using `bin/rake gettext:po_to_json` or `bin/rake gettext:compile`.
### Dynamic translations

View file

@ -147,8 +147,7 @@ ldd $(command -v git) | grep pcre2
The output should contain `libpcre2-8.so.0`.
Is the system packaged Git too old, or not compiled with pcre2?
Remove it:
If the system packaged Git is too old or not compiled with `pcre2`, remove it:
```shell
sudo apt-get remove git-core

View file

@ -70,7 +70,7 @@ This view allows you to:
- Filter image repositories by their name.
- [Delete](#delete-images-from-within-gitlab) one or more image repository.
- Navigate to the image repository details page.
- Show a **Quick start** dropdown with the most common commands to log in, build and push
- Show a **Quick start** dropdown with the most common commands to log in, build and push.
- Show a banner if the optional [cleanup policy](#cleanup-policy) is enabled for this project.
### Control Container Registry for your group

View file

@ -181,6 +181,7 @@ module Gitlab
successful_deployments: deployment_count(Deployment.success.where(last_28_days_time_period)),
failed_deployments: deployment_count(Deployment.failed.where(last_28_days_time_period)),
# rubocop: enable UsageData/LargeTable:
packages: count(::Packages::Package.where(last_28_days_time_period)),
personal_snippets: count(PersonalSnippet.where(last_28_days_time_period)),
project_snippets: count(ProjectSnippet.where(last_28_days_time_period))
}.tap do |data|

View file

@ -7779,6 +7779,9 @@ msgstr ""
msgid "DastProfiles|Do you want to discard your changes?"
msgstr ""
msgid "DastProfiles|Download validation text file"
msgstr ""
msgid "DastProfiles|Edit feature will come soon. Please create a new profile if changes needed"
msgstr ""
@ -7839,21 +7842,57 @@ msgstr ""
msgid "DastProfiles|Site Profiles"
msgstr ""
msgid "DastProfiles|Site is not validated yet, please follow the steps."
msgstr ""
msgid "DastProfiles|Site must be validated to run an active scan."
msgstr ""
msgid "DastProfiles|Spider timeout"
msgstr ""
msgid "DastProfiles|Step 1 - Choose site validation method"
msgstr ""
msgid "DastProfiles|Step 2 - Add following text to the target site"
msgstr ""
msgid "DastProfiles|Step 3 - Confirm text file location and validate"
msgstr ""
msgid "DastProfiles|Target URL"
msgstr ""
msgid "DastProfiles|Target timeout"
msgstr ""
msgid "DastProfiles|Text file validation"
msgstr ""
msgid "DastProfiles|The maximum number of seconds allowed for the site under test to respond to a request."
msgstr ""
msgid "DastProfiles|The maximum number of seconds allowed for the spider to traverse the site."
msgstr ""
msgid "DastProfiles|Validate"
msgstr ""
msgid "DastProfiles|Validate target site"
msgstr ""
msgid "DastProfiles|Validating..."
msgstr ""
msgid "DastProfiles|Validation failed, please make sure that you follow the steps above with the choosen method."
msgstr ""
msgid "DastProfiles|Validation must be turned off to change the target URL"
msgstr ""
msgid "DastProfiles|Validation succeeded. Both active and passive scans can be run against the target site."
msgstr ""
msgid "Data is still calculating..."
msgstr ""

View file

@ -46,7 +46,7 @@
"@gitlab/ui": "20.18.1",
"@gitlab/visual-review-tools": "1.6.1",
"@rails/actioncable": "^6.0.3-1",
"@sentry/browser": "^5.10.2",
"@sentry/browser": "^5.22.3",
"@sourcegraph/code-host-integration": "0.0.50",
"@toast-ui/editor": "^2.3.1",
"@toast-ui/vue-editor": "^2.3.1",

View file

@ -79,6 +79,23 @@ RSpec.describe SendFileUpload do
it_behaves_like 'handles image resize requests allowed by FFs'
end
context 'when boths FFs are enabled globally' do
before do
stub_feature_flags(dynamic_image_resizing_requester: true)
stub_feature_flags(dynamic_image_resizing_owner: true)
end
it_behaves_like 'handles image resize requests allowed by FFs'
context 'when current_user is nil' do
before do
allow(controller).to receive(:current_user).and_return(nil)
end
it_behaves_like 'handles image resize requests allowed by FFs'
end
end
context 'when only FF based on content requester is enabled for current user' do
before do
stub_feature_flags(dynamic_image_resizing_requester: image_requester)

View file

@ -101,6 +101,7 @@ FactoryBot.define do
create(:package, project: projects[0])
create(:package, project: projects[0])
create(:package, project: projects[1])
create(:package, created_at: 2.months.ago, project: projects[1])
ProjectFeature.first.update_attribute('repository_access_level', 0)

View file

@ -12,7 +12,7 @@
"title": { "type": "string" },
"confidential": { "type": "boolean" },
"due_date": { "type": ["date", "null"] },
"relative_position": { "type": "integer" },
"relative_position": { "type": ["integer", "null"] },
"time_estimate": { "type": "integer" },
"issue_sidebar_endpoint": { "type": "string" },
"toggle_subscription_endpoint": { "type": "string" },

View file

@ -1,59 +0,0 @@
import $ from 'jquery';
import AjaxLoadingSpinner from '~/ajax_loading_spinner';
describe('Ajax Loading Spinner', () => {
const fixtureTemplate = 'static/ajax_loading_spinner.html';
preloadFixtures(fixtureTemplate);
beforeEach(() => {
loadFixtures(fixtureTemplate);
AjaxLoadingSpinner.init();
});
it('change current icon with spinner icon and disable link while waiting ajax response', done => {
jest.spyOn($, 'ajax').mockImplementation(req => {
const xhr = new XMLHttpRequest();
const ajaxLoadingSpinner = document.querySelector('.js-ajax-loading-spinner');
const icon = ajaxLoadingSpinner.querySelector('i');
req.beforeSend(xhr, { dataType: 'text/html' });
expect(icon).not.toHaveClass('fa-trash-o');
expect(icon).toHaveClass('gl-spinner');
expect(icon).toHaveClass('gl-spinner-orange');
expect(icon).toHaveClass('gl-spinner-sm');
expect(icon.dataset.icon).toEqual('fa-trash-o');
expect(ajaxLoadingSpinner.getAttribute('disabled')).toEqual('');
req.complete({});
done();
const deferred = $.Deferred();
return deferred.promise();
});
document.querySelector('.js-ajax-loading-spinner').click();
});
it('use original icon again and enabled the link after complete the ajax request', done => {
jest.spyOn($, 'ajax').mockImplementation(req => {
const xhr = new XMLHttpRequest();
const ajaxLoadingSpinner = document.querySelector('.js-ajax-loading-spinner');
req.beforeSend(xhr, { dataType: 'text/html' });
req.complete({});
const icon = ajaxLoadingSpinner.querySelector('i');
expect(icon).toHaveClass('fa-trash-o');
expect(icon).not.toHaveClass('gl-spinner');
expect(icon).not.toHaveClass('gl-spinner-orange');
expect(icon).not.toHaveClass('gl-spinner-sm');
expect(ajaxLoadingSpinner.getAttribute('disabled')).toEqual(null);
done();
const deferred = $.Deferred();
return deferred.promise();
});
document.querySelector('.js-ajax-loading-spinner').click();
});
});

View file

@ -0,0 +1,32 @@
import AjaxLoadingSpinner from '~/branches/ajax_loading_spinner';
describe('Ajax Loading Spinner', () => {
let ajaxLoadingSpinnerElement;
let fauxEvent;
beforeEach(() => {
document.body.innerHTML = `
<div>
<a class="js-ajax-loading-spinner"
data-remote
href="http://goesnowhere.nothing/whereami">
<i class="fa fa-trash-o"></i>
</a></div>`;
AjaxLoadingSpinner.init();
ajaxLoadingSpinnerElement = document.querySelector('.js-ajax-loading-spinner');
fauxEvent = { target: ajaxLoadingSpinnerElement };
});
afterEach(() => {
document.body.innerHTML = '';
});
it('`ajaxBeforeSend` event handler sets current icon to spinner and disables link', () => {
expect(ajaxLoadingSpinnerElement.parentNode.querySelector('.gl-spinner')).toBeNull();
expect(ajaxLoadingSpinnerElement.classList.contains('hidden')).toBe(false);
AjaxLoadingSpinner.ajaxBeforeSend(fauxEvent);
expect(ajaxLoadingSpinnerElement.parentNode.querySelector('.gl-spinner')).not.toBeNull();
expect(ajaxLoadingSpinnerElement.classList.contains('hidden')).toBe(true);
});
});

View file

@ -1,3 +0,0 @@
<a class="js-ajax-loading-spinner" data-remote href="http://goesnowhere.nothing/whereami">
<i class="fa fa-trash-o"></i>
</a>

View file

@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils';
import { GlLink, GlSprintf } from '@gitlab/ui';
import DetailsRow from '~/registry/shared/components/details_row.vue';
import DetailsRow from '~/vue_shared/components/registry/details_row.vue';
import component from '~/packages/details/components/additional_metadata.vue';
import { mavenPackage, conanPackage, nugetPackage, npmPackage } from '../../mock_data';

View file

@ -1,7 +1,7 @@
import { shallowMount } from '@vue/test-utils';
import { GlLink, GlSprintf } from '@gitlab/ui';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import HistoryElement from '~/packages/details/components/history_element.vue';
import HistoryItem from '~/vue_shared/components/registry/history_item.vue';
import component from '~/packages/details/components/package_history.vue';
import { mavenPackage, mockPipelineInfo } from '../../mock_data';
@ -17,8 +17,8 @@ describe('Package History', () => {
wrapper = shallowMount(component, {
propsData: { ...defaultProps, ...props },
stubs: {
HistoryElement: {
props: HistoryElement.props,
HistoryItem: {
props: HistoryItem.props,
template: '<div data-testid="history-element"><slot></slot></div>',
},
GlSprintf,

View file

@ -6,7 +6,7 @@ import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import component from '~/registry/explorer/components/details_page/tags_list_row.vue';
import DeleteButton from '~/registry/explorer/components/delete_button.vue';
import DetailsRow from '~/registry/shared/components/details_row.vue';
import DetailsRow from '~/vue_shared/components/registry/details_row.vue';
import {
REMOVE_TAG_BUTTON_TITLE,
REMOVE_TAG_BUTTON_DISABLE_TOOLTIP,

View file

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`History Element renders the correct markup 1`] = `
exports[`History Item renders the correct markup 1`] = `
<li
class="timeline-entry system-note note-wrapper gl-mb-6!"
>
@ -31,7 +31,11 @@ exports[`History Element renders the correct markup 1`] = `
<div
class="note-body"
/>
>
<div
data-testid="body-slot"
/>
</div>
</div>
</div>
</li>

View file

@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils';
import { GlIcon } from '@gitlab/ui';
import component from '~/registry/shared/components/details_row.vue';
import component from '~/vue_shared/components/registry/details_row.vue';
describe('DetailsRow', () => {
let wrapper;

View file

@ -1,9 +1,9 @@
import { shallowMount } from '@vue/test-utils';
import { GlIcon } from '@gitlab/ui';
import component from '~/packages/details/components/history_element.vue';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import component from '~/vue_shared/components/registry/history_item.vue';
describe('History Element', () => {
describe('History Item', () => {
let wrapper;
const defaultProps = {
icon: 'pencil',
@ -17,6 +17,7 @@ describe('History Element', () => {
},
slots: {
default: '<div data-testid="default-slot"></div>',
body: '<div data-testid="body-slot"></div>',
},
});
};
@ -29,6 +30,7 @@ describe('History Element', () => {
const findTimelineEntry = () => wrapper.find(TimelineEntryItem);
const findGlIcon = () => wrapper.find(GlIcon);
const findDefaultSlot = () => wrapper.find('[data-testid="default-slot"]');
const findBodySlot = () => wrapper.find('[data-testid="body-slot"]');
it('renders the correct markup', () => {
mountComponent();
@ -41,11 +43,19 @@ describe('History Element', () => {
expect(findDefaultSlot().exists()).toBe(true);
});
it('has a body slot', () => {
mountComponent();
expect(findBodySlot().exists()).toBe(true);
});
it('has a timeline entry', () => {
mountComponent();
expect(findTimelineEntry().exists()).toBe(true);
});
it('has an icon', () => {
mountComponent();

View file

@ -28,5 +28,17 @@ RSpec.describe 'CarrierWave::Storage::Fog::File' do
expect(subject.authenticated_url).to eq("https://sa.blob.core.windows.net/test_container/test_blob?token")
end
end
context 'with custom expire_at' do
it 'properly sets expires param' do
expire_at = 24.hours.from_now
expect_next_instance_of(Fog::Storage::AzureRM::File) do |file|
expect(file).to receive(:url).with(expire_at).and_call_original
end
expect(subject.authenticated_url(expire_at: expire_at)).to eq("https://sa.blob.core.windows.net/test_container/test_blob?token")
end
end
end
end

View file

@ -479,7 +479,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
expect(count_data[:project_snippets]).to eq(4)
expect(count_data[:projects_with_packages]).to eq(2)
expect(count_data[:packages]).to eq(3)
expect(count_data[:packages]).to eq(4)
end
it 'gathers object store usage correctly' do
@ -572,6 +572,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
expect(counts_monthly[:snippets]).to eq(3)
expect(counts_monthly[:personal_snippets]).to eq(1)
expect(counts_monthly[:project_snippets]).to eq(2)
expect(counts_monthly[:packages]).to eq(3)
end
end

View file

@ -295,21 +295,15 @@ RSpec.describe Issuable do
end
describe "#new?" do
it "returns true when created today and record hasn't been updated" do
allow(issue).to receive(:today?).and_return(true)
it "returns false when created 30 hours ago" do
allow(issue).to receive(:created_at).and_return(Time.current - 30.hours)
expect(issue.new?).to be_falsey
end
it "returns true when created 20 hours ago" do
allow(issue).to receive(:created_at).and_return(Time.current - 20.hours)
expect(issue.new?).to be_truthy
end
it "returns false when not created today" do
allow(issue).to receive(:today?).and_return(false)
expect(issue.new?).to be_falsey
end
it "returns false when record has been updated" do
allow(issue).to receive(:today?).and_return(true)
issue.update_attribute(:updated_at, 1.hour.ago)
expect(issue.new?).to be_falsey
end
end
describe "#sort_by_attribute" do

View file

@ -11,7 +11,7 @@ RSpec.describe BambooService, :use_clean_rails_memory_store_caching do
let_it_be(:project) { create(:project) }
subject(:service) do
described_class.create(
described_class.create!(
project: project,
properties: {
bamboo_url: bamboo_url,
@ -85,7 +85,7 @@ RSpec.describe BambooService, :use_clean_rails_memory_store_caching do
bamboo_service = service
bamboo_service.bamboo_url = 'http://gitlab1.com'
bamboo_service.save
bamboo_service.save!
expect(bamboo_service.password).to be_nil
end
@ -94,7 +94,7 @@ RSpec.describe BambooService, :use_clean_rails_memory_store_caching do
bamboo_service = service
bamboo_service.username = 'some_name'
bamboo_service.save
bamboo_service.save!
expect(bamboo_service.password).to eq('password')
end
@ -104,7 +104,7 @@ RSpec.describe BambooService, :use_clean_rails_memory_store_caching do
bamboo_service.bamboo_url = 'http://gitlab_edited.com'
bamboo_service.password = 'password'
bamboo_service.save
bamboo_service.save!
expect(bamboo_service.password).to eq('password')
expect(bamboo_service.bamboo_url).to eq('http://gitlab_edited.com')
@ -117,7 +117,7 @@ RSpec.describe BambooService, :use_clean_rails_memory_store_caching do
bamboo_service.bamboo_url = 'http://gitlab_edited.com'
bamboo_service.password = 'password'
bamboo_service.save
bamboo_service.save!
expect(bamboo_service.password).to eq('password')
expect(bamboo_service.bamboo_url).to eq('http://gitlab_edited.com')

View file

@ -9,7 +9,7 @@ RSpec.describe BuildkiteService, :use_clean_rails_memory_store_caching do
let(:project) { create(:project) }
subject(:service) do
described_class.create(
described_class.create!(
project: project,
properties: {
service_hook: true,

View file

@ -28,7 +28,7 @@ RSpec.describe JiraService do
}
end
let(:service) { described_class.create(options) }
let(:service) { described_class.create!(options) }
it 'sets the URL properly' do
# jira-ruby gem parses the URI and handles trailing slashes fine:
@ -102,7 +102,7 @@ RSpec.describe JiraService do
}
end
subject { described_class.create(params) }
subject { described_class.create!(params) }
it 'does not store data into properties' do
expect(subject.properties).to be_nil
@ -189,7 +189,7 @@ RSpec.describe JiraService do
let_it_be(:new_url) { 'http://jira-new.example.com' }
before do
service.update(username: new_username, url: new_url)
service.update!(username: new_username, url: new_url)
end
it 'leaves properties field emtpy' do
@ -209,12 +209,12 @@ RSpec.describe JiraService do
context 'when updating the url, api_url, username, or password' do
it 'updates deployment type' do
service.update(url: 'http://first.url')
service.jira_tracker_data.update(deployment_type: 'server')
service.update!(url: 'http://first.url')
service.jira_tracker_data.update!(deployment_type: 'server')
expect(service.jira_tracker_data.deployment_server?).to be_truthy
service.update(api_url: 'http://another.url')
service.update!(api_url: 'http://another.url')
service.jira_tracker_data.reload
expect(service.jira_tracker_data.deployment_cloud?).to be_truthy
@ -222,25 +222,25 @@ RSpec.describe JiraService do
end
it 'calls serverInfo for url' do
service.update(url: 'http://first.url')
service.update!(url: 'http://first.url')
expect(WebMock).to have_requested(:get, /serverInfo/)
end
it 'calls serverInfo for api_url' do
service.update(api_url: 'http://another.url')
service.update!(api_url: 'http://another.url')
expect(WebMock).to have_requested(:get, /serverInfo/)
end
it 'calls serverInfo for username' do
service.update(username: 'test-user')
service.update!(username: 'test-user')
expect(WebMock).to have_requested(:get, /serverInfo/)
end
it 'calls serverInfo for password' do
service.update(password: 'test-password')
service.update!(password: 'test-password')
expect(WebMock).to have_requested(:get, /serverInfo/)
end
@ -248,7 +248,7 @@ RSpec.describe JiraService do
context 'when not updating the url, api_url, username, or password' do
it 'does not update deployment type' do
service.update(jira_issue_transition_id: 'jira_issue_transition_id')
expect {service.update!(jira_issue_transition_id: 'jira_issue_transition_id')}.to raise_error(ActiveRecord::RecordInvalid)
expect(WebMock).not_to have_requested(:get, /serverInfo/)
end
@ -268,7 +268,7 @@ RSpec.describe JiraService do
it 'resets password if url changed' do
service
service.url = 'http://jira_edited.example.com'
service.save
service.save!
expect(service.reload.url).to eq('http://jira_edited.example.com')
expect(service.password).to be_nil
@ -276,7 +276,7 @@ RSpec.describe JiraService do
it 'does not reset password if url "changed" to the same url as before' do
service.url = 'http://jira.example.com'
service.save
service.save!
expect(service.reload.url).to eq('http://jira.example.com')
expect(service.password).not_to be_nil
@ -284,7 +284,7 @@ RSpec.describe JiraService do
it 'resets password if url not changed but api url added' do
service.api_url = 'http://jira_edited.example.com/rest/api/2'
service.save
service.save!
expect(service.reload.api_url).to eq('http://jira_edited.example.com/rest/api/2')
expect(service.password).to be_nil
@ -293,7 +293,7 @@ RSpec.describe JiraService do
it 'does not reset password if new url is set together with password, even if it\'s the same password' do
service.url = 'http://jira_edited.example.com'
service.password = password
service.save
service.save!
expect(service.password).to eq(password)
expect(service.url).to eq('http://jira_edited.example.com')
@ -302,14 +302,14 @@ RSpec.describe JiraService do
it 'resets password if url changed, even if setter called multiple times' do
service.url = 'http://jira1.example.com/rest/api/2'
service.url = 'http://jira1.example.com/rest/api/2'
service.save
service.save!
expect(service.password).to be_nil
end
it 'does not reset password if username changed' do
service.username = 'some_name'
service.save
service.save!
expect(service.reload.password).to eq(password)
end
@ -317,7 +317,7 @@ RSpec.describe JiraService do
it 'does not reset password if password changed' do
service.url = 'http://jira_edited.example.com'
service.password = 'new_password'
service.save
service.save!
expect(service.reload.password).to eq('new_password')
end
@ -325,7 +325,7 @@ RSpec.describe JiraService do
it 'does not reset password if the password is touched and same as before' do
service.url = 'http://jira_edited.example.com'
service.password = password
service.save
service.save!
expect(service.reload.password).to eq(password)
end
@ -342,20 +342,20 @@ RSpec.describe JiraService do
it 'resets password if api url changed' do
service.api_url = 'http://jira_edited.example.com/rest/api/2'
service.save
service.save!
expect(service.password).to be_nil
end
it 'does not reset password if url changed' do
service.url = 'http://jira_edited.example.com'
service.save
service.save!
expect(service.password).to eq(password)
end
it 'resets password if api url set to empty' do
service.update(api_url: '')
service.update!(api_url: '')
expect(service.reload.password).to be_nil
end
@ -372,7 +372,7 @@ RSpec.describe JiraService do
it 'saves password if new url is set together with password' do
service.url = 'http://jira_edited.example.com/rest/api/2'
service.password = 'password'
service.save
service.save!
expect(service.reload.password).to eq('password')
expect(service.reload.url).to eq('http://jira_edited.example.com/rest/api/2')
end
@ -441,7 +441,7 @@ RSpec.describe JiraService do
allow_any_instance_of(JIRA::Resource::Issue).to receive(:key).and_return('JIRA-123')
allow(JIRA::Resource::Remotelink).to receive(:all).and_return([])
@jira_service.save
@jira_service.save!
project_issues_url = 'http://jira.example.com/rest/api/2/issue/JIRA-123'
@transitions_url = 'http://jira.example.com/rest/api/2/issue/JIRA-123/transitions'
@ -709,9 +709,11 @@ RSpec.describe JiraService do
describe '#test' do
let(:server_info_results) { { 'url' => 'http://url', 'deploymentType' => 'Cloud' } }
let_it_be(:project) { create(:project, :repository) }
let(:jira_service) do
described_class.new(
url: url,
project: project,
username: username,
password: password
)
@ -728,7 +730,7 @@ RSpec.describe JiraService do
end
it 'gets Jira project with API URL if set' do
jira_service.update(api_url: 'http://jira.api.com')
jira_service.update!(api_url: 'http://jira.api.com')
expect(server_info).to eq(success: true, result: server_info_results)
expect(WebMock).to have_requested(:get, /jira.api.com/)

View file

@ -33,7 +33,7 @@ RSpec.describe PackagistService do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
let(:push_sample_data) { Gitlab::DataBuilder::Push.build_sample(project, user) }
let(:packagist_service) { described_class.create(packagist_params) }
let(:packagist_service) { described_class.create!(packagist_params) }
before do
stub_request(:post, packagist_hook_url)

View file

@ -81,7 +81,7 @@ RSpec.describe PipelinesEmailService, :mailer do
context 'when pipeline is succeeded' do
before do
data[:object_attributes][:status] = 'success'
pipeline.update(status: 'success')
pipeline.update!(status: 'success')
end
it_behaves_like 'sending email'
@ -91,7 +91,7 @@ RSpec.describe PipelinesEmailService, :mailer do
context 'on default branch' do
before do
data[:object_attributes][:ref] = project.default_branch
pipeline.update(ref: project.default_branch)
pipeline.update!(ref: project.default_branch)
end
context 'notifications are enabled only for default branch' do
@ -115,7 +115,7 @@ RSpec.describe PipelinesEmailService, :mailer do
before do
create(:protected_branch, project: project, name: 'a-protected-branch')
data[:object_attributes][:ref] = 'a-protected-branch'
pipeline.update(ref: 'a-protected-branch')
pipeline.update!(ref: 'a-protected-branch')
end
context 'notifications are enabled only for default branch' do
@ -138,7 +138,7 @@ RSpec.describe PipelinesEmailService, :mailer do
context 'on a neither protected nor default branch' do
before do
data[:object_attributes][:ref] = 'a-random-branch'
pipeline.update(ref: 'a-random-branch')
pipeline.update!(ref: 'a-random-branch')
end
context 'notifications are enabled only for default branch' do
@ -177,7 +177,7 @@ RSpec.describe PipelinesEmailService, :mailer do
context 'with succeeded pipeline' do
before do
data[:object_attributes][:status] = 'success'
pipeline.update(status: 'success')
pipeline.update!(status: 'success')
end
it_behaves_like 'not sending email'
@ -195,7 +195,7 @@ RSpec.describe PipelinesEmailService, :mailer do
context 'with succeeded pipeline' do
before do
data[:object_attributes][:status] = 'success'
pipeline.update(status: 'success')
pipeline.update!(status: 'success')
end
it_behaves_like 'not sending email'
@ -206,7 +206,7 @@ RSpec.describe PipelinesEmailService, :mailer do
context 'on default branch' do
before do
data[:object_attributes][:ref] = project.default_branch
pipeline.update(ref: project.default_branch)
pipeline.update!(ref: project.default_branch)
end
context 'notifications are enabled only for default branch' do
@ -230,7 +230,7 @@ RSpec.describe PipelinesEmailService, :mailer do
before do
create(:protected_branch, project: project, name: 'a-protected-branch')
data[:object_attributes][:ref] = 'a-protected-branch'
pipeline.update(ref: 'a-protected-branch')
pipeline.update!(ref: 'a-protected-branch')
end
context 'notifications are enabled only for default branch' do
@ -253,7 +253,7 @@ RSpec.describe PipelinesEmailService, :mailer do
context 'on a neither protected nor default branch' do
before do
data[:object_attributes][:ref] = 'a-random-branch'
pipeline.update(ref: 'a-random-branch')
pipeline.update!(ref: 'a-random-branch')
end
context 'notifications are enabled only for default branch' do
@ -281,7 +281,7 @@ RSpec.describe PipelinesEmailService, :mailer do
context 'with failed pipeline' do
before do
data[:object_attributes][:status] = 'failed'
pipeline.update(status: 'failed')
pipeline.update!(status: 'failed')
end
it_behaves_like 'not sending email'
@ -295,7 +295,7 @@ RSpec.describe PipelinesEmailService, :mailer do
context 'with failed pipeline' do
before do
data[:object_attributes][:status] = 'failed'
pipeline.update(status: 'failed')
pipeline.update!(status: 'failed')
end
it_behaves_like 'sending email'

View file

@ -11,7 +11,7 @@ RSpec.describe TeamcityService, :use_clean_rails_memory_store_caching do
let(:project) { create(:project) }
subject(:service) do
described_class.create(
described_class.create!(
project: project,
properties: {
teamcity_url: teamcity_url,
@ -85,7 +85,7 @@ RSpec.describe TeamcityService, :use_clean_rails_memory_store_caching do
teamcity_service = service
teamcity_service.teamcity_url = 'http://gitlab1.com'
teamcity_service.save
teamcity_service.save!
expect(teamcity_service.password).to be_nil
end
@ -94,7 +94,7 @@ RSpec.describe TeamcityService, :use_clean_rails_memory_store_caching do
teamcity_service = service
teamcity_service.username = 'some_name'
teamcity_service.save
teamcity_service.save!
expect(teamcity_service.password).to eq('password')
end
@ -104,7 +104,7 @@ RSpec.describe TeamcityService, :use_clean_rails_memory_store_caching do
teamcity_service.teamcity_url = 'http://gitlab_edited.com'
teamcity_service.password = 'password'
teamcity_service.save
teamcity_service.save!
expect(teamcity_service.password).to eq('password')
expect(teamcity_service.teamcity_url).to eq('http://gitlab_edited.com')
@ -117,7 +117,7 @@ RSpec.describe TeamcityService, :use_clean_rails_memory_store_caching do
teamcity_service.teamcity_url = 'http://gitlab_edited.com'
teamcity_service.password = 'password'
teamcity_service.save
teamcity_service.save!
expect(teamcity_service.password).to eq('password')
expect(teamcity_service.teamcity_url).to eq('http://gitlab_edited.com')

View file

@ -75,35 +75,10 @@ RSpec.describe Issues::CreateService do
expect(Todo.where(attributes).count).to eq 1
end
it 'rebalances if needed' do
create(:issue, project: project, relative_position: RelativePositioning::MAX_POSITION)
expect(IssueRebalancingWorker).to receive(:perform_async).with(nil, project.id)
it 'moves the issue to the end, in an asynchronous worker' do
expect(IssuePlacementWorker).to receive(:perform_async).with(Integer)
expect(issue.relative_position).to eq(project.issues.maximum(:relative_position))
end
it 'does not rebalance if the flag is disabled' do
stub_feature_flags(rebalance_issues: false)
create(:issue, project: project, relative_position: RelativePositioning::MAX_POSITION)
expect(IssueRebalancingWorker).not_to receive(:perform_async)
expect(issue.relative_position).to eq(project.issues.maximum(:relative_position))
end
it 'does rebalance if the flag is enabled for the project' do
stub_feature_flags(rebalance_issues: project)
create(:issue, project: project, relative_position: RelativePositioning::MAX_POSITION)
expect(IssueRebalancingWorker).to receive(:perform_async).with(nil, project.id)
expect(issue.relative_position).to eq(project.issues.maximum(:relative_position))
end
it 'does not rebalance unless needed' do
expect(IssueRebalancingWorker).not_to receive(:perform_async)
expect(issue.relative_position).to eq(project.issues.maximum(:relative_position))
described_class.new(project, user, opts).execute
end
context 'when label belongs to project group' do

View file

@ -3,9 +3,9 @@
require 'spec_helper'
RSpec.describe Issues::ReorderService do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:user) { create_default(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:project, reload: true) { create(:project, namespace: group) }
shared_examples 'issues reorder service' do
context 'when reordering issues' do
@ -14,7 +14,7 @@ RSpec.describe Issues::ReorderService do
end
it 'returns false with both invalid params' do
params = { move_after_id: nil, move_before_id: 1 }
params = { move_after_id: nil, move_before_id: non_existing_record_id }
expect(service(params).execute(issue1)).to be_falsey
end
@ -27,27 +27,39 @@ RSpec.describe Issues::ReorderService do
expect(issue1.relative_position)
.to be_between(issue2.relative_position, issue3.relative_position)
end
it 'sorts issues if only given one neighbour, on the left' do
params = { move_before_id: issue3.id }
service(params).execute(issue1)
expect(issue1.relative_position).to be > issue3.relative_position
end
it 'sorts issues if only given one neighbour, on the right' do
params = { move_after_id: issue1.id }
service(params).execute(issue3)
expect(issue3.relative_position).to be < issue1.relative_position
end
end
end
describe '#execute' do
let(:issue1) { create(:issue, project: project, relative_position: 10) }
let(:issue2) { create(:issue, project: project, relative_position: 20) }
let(:issue3) { create(:issue, project: project, relative_position: 30) }
let_it_be(:issue1, reload: true) { create(:issue, project: project, relative_position: 10) }
let_it_be(:issue2) { create(:issue, project: project, relative_position: 20) }
let_it_be(:issue3, reload: true) { create(:issue, project: project, relative_position: 30) }
context 'when ordering issues in a project' do
let(:parent) { project }
before do
parent.add_developer(user)
project.add_developer(user)
end
it_behaves_like 'issues reorder service'
end
context 'when ordering issues in a group' do
let(:project) { create(:project, namespace: group) }
before do
group.add_developer(user)
end

View file

@ -0,0 +1,69 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe IssuePlacementWorker do
describe '#perform' do
let_it_be(:time) { Time.now.utc }
let_it_be(:project) { create(:project) }
let_it_be(:author) { create(:user) }
let_it_be(:common_attrs) { { author: author, project: project } }
let_it_be(:unplaced) { common_attrs.merge(relative_position: nil) }
let_it_be(:issue) { create(:issue, **unplaced, created_at: time) }
let_it_be(:issue_a) { create(:issue, **unplaced, created_at: time - 1.minute) }
let_it_be(:issue_b) { create(:issue, **unplaced, created_at: time - 2.minutes) }
let_it_be(:issue_c) { create(:issue, **unplaced, created_at: time + 1.minute) }
let_it_be(:issue_d) { create(:issue, **unplaced, created_at: time + 2.minutes) }
let_it_be(:issue_e) { create(:issue, **common_attrs, relative_position: 10, created_at: time + 1.minute) }
let_it_be(:issue_f) { create(:issue, **unplaced, created_at: time + 1.minute) }
let_it_be(:irrelevant) { create(:issue, relative_position: nil, created_at: time) }
it 'places all issues created at most 5 minutes before this one at the end, most recent last' do
expect do
described_class.new.perform(issue.id)
end.not_to change { irrelevant.reset.relative_position }
expect(project.issues.order_relative_position_asc)
.to eq([issue_e, issue_b, issue_a, issue, issue_c, issue_f, issue_d])
expect(project.issues.where(relative_position: nil)).not_to exist
end
it 'schedules rebalancing if needed' do
issue_a.update!(relative_position: RelativePositioning::MAX_POSITION)
expect(IssueRebalancingWorker).to receive(:perform_async).with(nil, project.id)
described_class.new.perform(issue.id)
end
it 'limits the sweep to QUERY_LIMIT records' do
# Ensure there are more than N issues in this set
n = described_class::QUERY_LIMIT
create_list(:issue, n - 5, **unplaced)
expect(Issue).to receive(:move_nulls_to_end).with(have_attributes(count: n)).and_call_original
described_class.new.perform(issue.id)
expect(project.issues.where(relative_position: nil)).to exist
end
it 'anticipates the failure to find the issue' do
id = non_existing_record_id
expect { described_class.new.perform(id) }.not_to raise_error
end
it 'anticipates the failure to place the issues, and schedules rebalancing' do
allow(Issue).to receive(:move_nulls_to_end) { raise RelativePositioning::NoSpaceLeft }
expect(IssueRebalancingWorker).to receive(:perform_async).with(nil, project.id)
expect(Gitlab::ErrorTracking)
.to receive(:log_exception)
.with(RelativePositioning::NoSpaceLeft, issue_id: issue.id)
described_class.new.perform(issue.id)
end
end
end

View file

@ -39,10 +39,10 @@ RSpec.describe NewIssueWorker do
end
context 'when everything is ok' do
let(:project) { create(:project, :public) }
let(:mentioned) { create(:user) }
let(:user) { create(:user) }
let(:issue) { create(:issue, project: project, description: "issue for #{mentioned.to_reference}") }
let_it_be(:user) { create_default(:user) }
let_it_be(:project) { create(:project, :public) }
let_it_be(:mentioned) { create(:user) }
let_it_be(:issue) { create(:issue, project: project, description: "issue for #{mentioned.to_reference}") }
it 'creates a new event record' do
expect { worker.perform(issue.id, user.id) }.to change { Event.count }.from(0).to(1)

View file

@ -1076,56 +1076,56 @@
resolved "https://registry.yarnpkg.com/@rails/actioncable/-/actioncable-6.0.3-1.tgz#9b9eb8858a6507162911007d355d9a206e1c5caa"
integrity sha512-szFhWD+V5TAxVNVIG16klgq+ypqA5k5AecLarTTrXgOG8cawVbQdOAwLbCmzkwiQ60rGSxAFoC1u2LrzxSK2Aw==
"@sentry/browser@^5.10.2":
version "5.10.2"
resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-5.10.2.tgz#0bbb05505c58ea998c833cffec3f922fe4b4fa58"
integrity sha512-r3eyBu2ln7odvWtXARCZPzpuGrKsD6U9F3gKTu4xdFkA0swSLUvS7AC2FUksj/1BE23y+eB/zzPT+RYJ58tidA==
"@sentry/browser@^5.22.3":
version "5.22.3"
resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-5.22.3.tgz#7a64bd1cf01bf393741a3e4bf35f82aa927f5b4e"
integrity sha512-2TzE/CoBa5ZkvxJizDdi1Iz1ldmXSJpFQ1mL07PIXBjCt0Wxf+WOuFSj5IP4L40XHfJE5gU8wEvSH0VDR8nXtA==
dependencies:
"@sentry/core" "5.10.2"
"@sentry/types" "5.10.0"
"@sentry/utils" "5.10.2"
"@sentry/core" "5.22.3"
"@sentry/types" "5.22.3"
"@sentry/utils" "5.22.3"
tslib "^1.9.3"
"@sentry/core@5.10.2":
version "5.10.2"
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-5.10.2.tgz#1cb64489e6f8363c3249415b49d3f1289814825f"
integrity sha512-sKVeFH3v8K8xw2vM5MKMnnyAAwih+JSE3pbNL0CcCCA+/SwX+3jeAo2BhgXev2SAR/TjWW+wmeC9TdIW7KyYbg==
"@sentry/core@5.22.3":
version "5.22.3"
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-5.22.3.tgz#030f435f2b518f282ba8bd954dac90cd70888bd7"
integrity sha512-eGL5uUarw3o4i9QUb9JoFHnhriPpWCaqeaIBB06HUpdcvhrjoowcKZj1+WPec5lFg5XusE35vez7z/FPzmJUDw==
dependencies:
"@sentry/hub" "5.10.2"
"@sentry/minimal" "5.10.2"
"@sentry/types" "5.10.0"
"@sentry/utils" "5.10.2"
"@sentry/hub" "5.22.3"
"@sentry/minimal" "5.22.3"
"@sentry/types" "5.22.3"
"@sentry/utils" "5.22.3"
tslib "^1.9.3"
"@sentry/hub@5.10.2":
version "5.10.2"
resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-5.10.2.tgz#25d9f36b8f7c5cb65cf486737fa61dc9bf69b7e3"
integrity sha512-hSlZIiu3hcR/I5yEhlpN9C0nip+U7hiRzRzUQaBiHO4YG4TC58NqnOPR89D/ekiuHIXzFpjW9OQmqtAMRoSUYA==
"@sentry/hub@5.22.3":
version "5.22.3"
resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-5.22.3.tgz#08309a70d2ea8d5e313d05840c1711f34f2fffe5"
integrity sha512-INo47m6N5HFEs/7GMP9cqxOIt7rmRxdERunA3H2L37owjcr77MwHVeeJ9yawRS6FMtbWXplgWTyTIWIYOuqVbw==
dependencies:
"@sentry/types" "5.10.0"
"@sentry/utils" "5.10.2"
"@sentry/types" "5.22.3"
"@sentry/utils" "5.22.3"
tslib "^1.9.3"
"@sentry/minimal@5.10.2":
version "5.10.2"
resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-5.10.2.tgz#267c2f3aa6877a0fe7a86971942e83f3ee616580"
integrity sha512-GalixiM9sckYfompH5HHTp9XT2BcjawBkcl1DMEKUBEi37+kUq0bivOBmnN1G/I4/wWOUdnAI/kagDWaWpbZPg==
"@sentry/minimal@5.22.3":
version "5.22.3"
resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-5.22.3.tgz#706e4029ae5494123d3875c658ba8911aa5cc440"
integrity sha512-HoINpYnVYCpNjn2XIPIlqH5o4BAITpTljXjtAftOx6Hzj+Opjg8tR8PWliyKDvkXPpc4kXK9D6TpEDw8MO0wZA==
dependencies:
"@sentry/hub" "5.10.2"
"@sentry/types" "5.10.0"
"@sentry/hub" "5.22.3"
"@sentry/types" "5.22.3"
tslib "^1.9.3"
"@sentry/types@5.10.0":
version "5.10.0"
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-5.10.0.tgz#4f0ba31b6e4d5371112c38279f11f66c73b43746"
integrity sha512-TW20GzkCWsP6uAxR2JIpIkiitCKyIOfkyDsKBeLqYj4SaZjfvBPnzgNCcYR0L0UsP1/Es6oHooZfIGSkp6GGxQ==
"@sentry/types@5.22.3":
version "5.22.3"
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-5.22.3.tgz#d1d547b30ee8bd7771fa893af74c4f3d71f0fd18"
integrity sha512-cv+VWK0YFgCVDvD1/HrrBWOWYG3MLuCUJRBTkV/Opdy7nkdNjhCAJQrEyMM9zX0sac8FKWKOHT0sykNh8KgmYw==
"@sentry/utils@5.10.2":
version "5.10.2"
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-5.10.2.tgz#261f575079d30aaf604e59f5f4de0aa21db22252"
integrity sha512-UcbbaFpYrGSV448lQ16Cr+W/MPuKUflQQUdrMCt5vgaf5+M7kpozlcji4GGGZUCXIA7oRP93ABoXj55s1OM9zw==
"@sentry/utils@5.22.3":
version "5.22.3"
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-5.22.3.tgz#e3bda3e789239eb16d436f768daa12829f33d18f"
integrity sha512-AHNryXMBvIkIE+GQxTlmhBXD0Ksh+5w1SwM5qi6AttH+1qjWLvV6WB4+4pvVvEoS8t5F+WaVUZPQLmCCWp6zKw==
dependencies:
"@sentry/types" "5.10.0"
"@sentry/types" "5.22.3"
tslib "^1.9.3"
"@sindresorhus/is@^0.14.0":