Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
35d5ae4e3d
commit
4c083c8163
2
Gemfile
2
Gemfile
|
@ -233,7 +233,7 @@ gem 'js_regex', '~> 3.7'
|
|||
gem 'device_detector'
|
||||
|
||||
# Redis
|
||||
gem 'redis', '~> 4.4.0'
|
||||
gem 'redis', '~> 4.7.0'
|
||||
gem 'connection_pool', '~> 2.0'
|
||||
|
||||
# Redis session store
|
||||
|
|
|
@ -1098,7 +1098,7 @@ GEM
|
|||
json
|
||||
recursive-open-struct (1.1.3)
|
||||
redcarpet (3.5.1)
|
||||
redis (4.4.0)
|
||||
redis (4.7.1)
|
||||
redis-actionpack (5.3.0)
|
||||
actionpack (>= 5, < 8)
|
||||
redis-rack (>= 2.1.0, < 3)
|
||||
|
@ -1701,7 +1701,7 @@ DEPENDENCIES
|
|||
rdoc (~> 6.3.2)
|
||||
re2 (~> 1.4.0)
|
||||
recaptcha (~> 4.11)
|
||||
redis (~> 4.4.0)
|
||||
redis (~> 4.7.0)
|
||||
redis-actionpack (~> 5.3.0)
|
||||
redis-namespace (~> 1.8.1)
|
||||
request_store (~> 1.5)
|
||||
|
|
|
@ -63,6 +63,7 @@ export default () => {
|
|||
const isMarkdown = editBlobForm.data('is-markdown');
|
||||
const previewMarkdownPath = editBlobForm.data('previewMarkdownPath');
|
||||
const commitButton = $('.js-commit-button');
|
||||
const commitButtonLoading = $('.js-commit-button-loading');
|
||||
const cancelLink = $('#cancel-changes');
|
||||
|
||||
import('./edit_blob')
|
||||
|
@ -88,6 +89,8 @@ export default () => {
|
|||
});
|
||||
|
||||
commitButton.on('click', () => {
|
||||
commitButton.addClass('gl-display-none');
|
||||
commitButtonLoading.removeClass('gl-display-none');
|
||||
window.onbeforeunload = null;
|
||||
});
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@ export default {
|
|||
</script>
|
||||
<template>
|
||||
<div v-if="!calloutDismissed" class="pipeline-schedules-user-callout user-callout">
|
||||
<div class="bordered-box landing content-block" data-testid="innerContent">
|
||||
<div class="bordered-box landing content-block gl-p-5!" data-testid="innerContent">
|
||||
<gl-button
|
||||
category="tertiary"
|
||||
icon="close"
|
||||
|
|
|
@ -282,7 +282,7 @@ export default {
|
|||
const descriptions = {};
|
||||
|
||||
Object.entries(data).forEach(([key, { value, description }]) => {
|
||||
if (description !== null) {
|
||||
if (description) {
|
||||
params[key] = value;
|
||||
descriptions[key] = description;
|
||||
}
|
||||
|
|
|
@ -232,7 +232,11 @@ export default {
|
|||
</span>
|
||||
</div>
|
||||
<div class="issuable-info">
|
||||
<work-item-type-icon v-if="showWorkItemTypeIcon" :work-item-type="issuable.type" />
|
||||
<work-item-type-icon
|
||||
v-if="showWorkItemTypeIcon"
|
||||
:work-item-type="issuable.type"
|
||||
show-tooltip-on-hover
|
||||
/>
|
||||
<slot v-if="hasSlotContents('reference')" name="reference"></slot>
|
||||
<span v-else data-testid="issuable-reference" class="issuable-reference">
|
||||
{{ reference }}
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
<script>
|
||||
import { GlIcon } from '@gitlab/ui';
|
||||
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
|
||||
import { WORK_ITEMS_TYPE_MAP } from '../constants';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlIcon,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
props: {
|
||||
workItemType: {
|
||||
type: String,
|
||||
|
@ -22,6 +25,11 @@ export default {
|
|||
required: false,
|
||||
default: '',
|
||||
},
|
||||
showTooltipOnHover: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
iconName() {
|
||||
|
@ -32,13 +40,21 @@ export default {
|
|||
workItemTypeName() {
|
||||
return WORK_ITEMS_TYPE_MAP[this.workItemType]?.name;
|
||||
},
|
||||
workItemTooltipTitle() {
|
||||
return this.showTooltipOnHover ? this.workItemTypeName : '';
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span>
|
||||
<gl-icon :name="iconName" class="gl-mr-2" />
|
||||
<gl-icon
|
||||
v-gl-tooltip.hover="showTooltipOnHover"
|
||||
:name="iconName"
|
||||
:title="workItemTooltipTitle"
|
||||
class="gl-mr-2"
|
||||
/>
|
||||
<span v-if="workItemTypeName" :class="{ 'gl-sr-only': !showText }">{{ workItemTypeName }}</span>
|
||||
</span>
|
||||
</template>
|
||||
|
|
|
@ -28,13 +28,6 @@
|
|||
.pipeline-schedule-table-row {
|
||||
.branch-name-cell {
|
||||
max-width: 300px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.next-run-cell {
|
||||
color: var(--gray-500, $gray-500);
|
||||
}
|
||||
|
||||
a {
|
||||
|
@ -50,7 +43,6 @@
|
|||
.bordered-box.content-block {
|
||||
border: 1px solid var(--border-color, $border-color);
|
||||
background-color: transparent;
|
||||
padding: $gl-spacing-scale-5;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -459,7 +459,7 @@ a.gl-badge.badge-warning:active {
|
|||
.gl-form-input:disabled,
|
||||
.gl-form-input.form-control:disabled {
|
||||
cursor: not-allowed;
|
||||
color: #868686;
|
||||
color: #999;
|
||||
}
|
||||
.gl-form-input::placeholder,
|
||||
.gl-form-input.form-control::placeholder {
|
||||
|
|
|
@ -438,7 +438,7 @@ a.gl-badge.badge-warning:active {
|
|||
.gl-form-input:disabled,
|
||||
.gl-form-input.form-control:disabled {
|
||||
cursor: not-allowed;
|
||||
color: #868686;
|
||||
color: #666;
|
||||
}
|
||||
.gl-form-input::placeholder,
|
||||
.gl-form-input.form-control::placeholder {
|
||||
|
|
|
@ -245,7 +245,7 @@ fieldset:disabled a.btn {
|
|||
.gl-form-input:disabled,
|
||||
.gl-form-input.form-control:disabled {
|
||||
cursor: not-allowed;
|
||||
color: #868686;
|
||||
color: #666;
|
||||
}
|
||||
.gl-form-input::placeholder,
|
||||
.gl-form-input.form-control::placeholder {
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
%div{ formatted_options }
|
||||
.row
|
||||
.col-lg-4
|
||||
%h4.gl-mt-0
|
||||
= title
|
||||
- if description?
|
||||
%p
|
||||
= description
|
||||
.col-lg-8
|
||||
= body
|
|
@ -0,0 +1,22 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Layouts
|
||||
class HorizontalSectionComponent < ViewComponent::Base
|
||||
# @param [Boolean] border
|
||||
# @param [Hash] options
|
||||
def initialize(border: true, options: {})
|
||||
@border = border
|
||||
@options = options
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
renders_one :title
|
||||
renders_one :description
|
||||
renders_one :body
|
||||
|
||||
def formatted_options
|
||||
@options.merge({ class: [('gl-border-b' if @border), @options[:class]].flatten.compact })
|
||||
end
|
||||
end
|
||||
end
|
|
@ -112,7 +112,7 @@ module Pajamas
|
|||
def base_attributes
|
||||
attributes = {}
|
||||
|
||||
attributes['disabled'] = '' if @disabled || @loading
|
||||
attributes['disabled'] = 'disabled' if @disabled || @loading
|
||||
attributes['aria-disabled'] = true if @disabled || @loading
|
||||
attributes['type'] = @type unless @href
|
||||
|
||||
|
|
|
@ -83,21 +83,21 @@ class ActiveSession
|
|||
is_impersonated: request.session[:impersonator_id].present?
|
||||
)
|
||||
|
||||
redis.pipelined do
|
||||
redis.setex(
|
||||
redis.pipelined do |pipeline|
|
||||
pipeline.setex(
|
||||
key_name(user.id, session_private_id),
|
||||
expiry,
|
||||
active_user_session.dump
|
||||
)
|
||||
|
||||
# Deprecated legacy format - temporary to support mixed deployments
|
||||
redis.setex(
|
||||
pipeline.setex(
|
||||
key_name_v1(user.id, session_private_id),
|
||||
expiry,
|
||||
Marshal.dump(active_user_session)
|
||||
)
|
||||
|
||||
redis.sadd(
|
||||
pipeline.sadd(
|
||||
lookup_key_name(user.id),
|
||||
session_private_id
|
||||
)
|
||||
|
|
|
@ -1,42 +1,34 @@
|
|||
= gitlab_ui_form_for [:admin, @group] do |f|
|
||||
= form_errors(@group)
|
||||
.gl-border-b.gl-mb-6
|
||||
.row
|
||||
.col-lg-4
|
||||
%h4.gl-mt-0
|
||||
= _('Naming, visibility')
|
||||
%p
|
||||
= _('Update your group name, description, avatar, and visibility.')
|
||||
= link_to _('Learn more about groups.'), help_page_path('user/group/index')
|
||||
.col-lg-8
|
||||
= render 'shared/groups/group_name_and_path_fields', f: f
|
||||
= render 'shared/group_form_description', f: f
|
||||
.form-group.gl-form-group{ role: 'group' }
|
||||
= f.label :avatar, _("Group avatar"), class: 'gl-display-block col-form-label'
|
||||
= render 'shared/choose_avatar_button', f: f
|
||||
= render 'shared/old_visibility_level', f: f, visibility_level: visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group, with_label: false
|
||||
= render ::Layouts::HorizontalSectionComponent.new(options: { class: 'gl-mb-6' }) do |c|
|
||||
= c.title { _('Naming, visibility') }
|
||||
= c.description do
|
||||
= _('Update your group name, description, avatar, and visibility.')
|
||||
= link_to _('Learn more about groups.'), help_page_path('user/group/index')
|
||||
= c.body do
|
||||
= render 'shared/groups/group_name_and_path_fields', f: f
|
||||
= render 'shared/group_form_description', f: f
|
||||
.form-group.gl-form-group{ role: 'group' }
|
||||
= f.label :avatar, _("Group avatar"), class: 'gl-display-block col-form-label'
|
||||
= render 'shared/choose_avatar_button', f: f
|
||||
= render 'shared/old_visibility_level', f: f, visibility_level: visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group, with_label: false
|
||||
|
||||
.gl-border-b.gl-pb-3.gl-mb-6
|
||||
.row
|
||||
.col-lg-4
|
||||
%h4.gl-mt-0
|
||||
= _('Permissions and group features')
|
||||
%p
|
||||
= _('Configure advanced permissions, Large File Storage, two-factor authentication, and CI/CD settings.')
|
||||
.col-lg-8
|
||||
= render_if_exists 'shared/old_repository_size_limit_setting', form: f, type: :group
|
||||
= render_if_exists 'admin/namespace_plan', f: f
|
||||
.form-group.gl-form-group{ role: 'group' }
|
||||
= render 'shared/allow_request_access', form: f
|
||||
= render 'groups/group_admin_settings', f: f
|
||||
= render_if_exists 'namespaces/shared_runners_minutes_settings', group: @group, form: f
|
||||
.gl-mb-3
|
||||
.row
|
||||
.col-lg-4
|
||||
%h4.gl-mt-0
|
||||
= _('Admin notes')
|
||||
.col-lg-8
|
||||
= render 'shared/admin/admin_note_form', f: f
|
||||
= render ::Layouts::HorizontalSectionComponent.new(options: { class: 'gl-pb-3 gl-mb-6' }) do |c|
|
||||
= c.title { _('Permissions and group features') }
|
||||
= c.description do
|
||||
= _('Configure advanced permissions, Large File Storage, two-factor authentication, and CI/CD settings.')
|
||||
= c.body do
|
||||
= render_if_exists 'shared/old_repository_size_limit_setting', form: f, type: :group
|
||||
= render_if_exists 'admin/namespace_plan', f: f
|
||||
.form-group.gl-form-group{ role: 'group' }
|
||||
= render 'shared/allow_request_access', form: f
|
||||
= render 'groups/group_admin_settings', f: f
|
||||
= render_if_exists 'namespaces/shared_runners_minutes_settings', group: @group, form: f
|
||||
|
||||
= render ::Layouts::HorizontalSectionComponent.new(border: false, options: { class: 'gl-pb-3' }) do |c|
|
||||
= c.title { _('Admin notes') }
|
||||
= c.body do
|
||||
= render 'shared/admin/admin_note_form', f: f
|
||||
|
||||
- if @group.new_record?
|
||||
= render Pajamas::AlertComponent.new(dismissible: false) do |c|
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
- if Feature.enabled?(:restyle_login_page, @project)
|
||||
.gl-text-center.gl-pt-5
|
||||
%label.gl-font-weight-normal
|
||||
= _("Create an account using:")
|
||||
= _("Register with:")
|
||||
.gl-text-center.gl-w-90p.gl-ml-auto.gl-mr-auto
|
||||
- providers.each do |provider|
|
||||
= link_to omniauth_authorize_path(:user, provider, register_omniauth_params), method: :post, class: "btn gl-button btn-default gl-ml-2 gl-mr-2 gl-mb-2 js-oauth-login #{qa_class_for_provider(provider)}", data: { provider: provider }, id: "oauth-login-#{provider}" do
|
||||
|
|
|
@ -1,9 +1,17 @@
|
|||
- return unless Gitlab::CurrentSettings.current_application_settings.enforce_terms?
|
||||
|
||||
%p.gl-text-gray-500.gl-mt-5.gl-mb-0
|
||||
- if Gitlab.com?
|
||||
= html_escape(s_("SignUp|By clicking %{button_text}, I agree that I have read and accepted the GitLab %{link_start}Terms of Use and Privacy Policy%{link_end}")) % { button_text: button_text,
|
||||
link_start: "<a href='#{terms_path}' target='_blank' rel='noreferrer noopener'>".html_safe, link_end: '</a>'.html_safe }
|
||||
- if Feature.enabled?(:restyle_login_page, @project)
|
||||
- if Gitlab.com?
|
||||
= html_escape(s_("SignUp|By clicking %{button_text} or registering through a third party you accept the GitLab%{link_start} Terms of Use and acknowledge the Privacy Policy and Cookie Policy%{link_end}")) % { button_text: button_text,
|
||||
link_start: "<a href='#{terms_path}' target='_blank' rel='noreferrer noopener'>".html_safe, link_end: '</a>'.html_safe }
|
||||
- else
|
||||
= html_escape(s_("SignUp|By clicking %{button_text} or registering through a third party you accept the%{link_start} Terms of Use and acknowledge the Privacy Policy and Cookie Policy%{link_end}")) % { button_text: button_text,
|
||||
link_start: "<a href='#{terms_path}' target='_blank' rel='noreferrer noopener'>".html_safe, link_end: '</a>'.html_safe }
|
||||
- else
|
||||
= html_escape(s_("SignUp|By clicking %{button_text}, I agree that I have read and accepted the %{link_start}Terms of Use and Privacy Policy%{link_end}")) % { button_text: button_text,
|
||||
link_start: "<a href='#{terms_path}' target='_blank' rel='noreferrer noopener'>".html_safe, link_end: '</a>'.html_safe }
|
||||
- if Gitlab.com?
|
||||
= html_escape(s_("SignUp|By clicking %{button_text}, I agree that I have read and accepted the GitLab %{link_start}Terms of Use and Privacy Policy%{link_end}")) % { button_text: button_text,
|
||||
link_start: "<a href='#{terms_path}' target='_blank' rel='noreferrer noopener'>".html_safe, link_end: '</a>'.html_safe }
|
||||
- else
|
||||
= html_escape(s_("SignUp|By clicking %{button_text}, I agree that I have read and accepted the %{link_start}Terms of Use and Privacy Policy%{link_end}")) % { button_text: button_text,
|
||||
link_start: "<a href='#{terms_path}' target='_blank' rel='noreferrer noopener'>".html_safe, link_end: '</a>'.html_safe }
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
.form-actions.gl-display-flex
|
||||
= render Pajamas::ButtonComponent.new(type: :submit, variant: :confirm, button_options: { id: 'commit-changes', class: 'js-commit-button', data: { qa_selector: 'commit_button' } }) do
|
||||
- submit_button_options = { type: :submit, variant: :confirm, button_options: { id: 'commit-changes', class: 'js-commit-button', data: { qa_selector: 'commit_button' } } }
|
||||
= render Pajamas::ButtonComponent.new(**submit_button_options) do
|
||||
= _('Commit changes')
|
||||
= render Pajamas::ButtonComponent.new(loading: true, disabled: true, **submit_button_options.merge({ button_options: { class: 'js-commit-button-loading gl-display-none' } })) do
|
||||
= _('Commit changes')
|
||||
|
||||
= render Pajamas::ButtonComponent.new(href: cancel_path, button_options: { class: 'gl-ml-3', id: 'cancel-changes', aria: { label: _('Discard changes') }, data: { confirm: leave_edit_message, confirm_btn_variant: "danger" } }) do
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
%tr.pipeline-schedule-table-row
|
||||
%td
|
||||
= pipeline_schedule.description
|
||||
%td.branch-name-cell
|
||||
%td.branch-name-cell.gl-text-truncate
|
||||
- if pipeline_schedule.for_tag?
|
||||
= sprite_icon('tag', size: 12)
|
||||
- else
|
||||
|
@ -17,7 +17,7 @@
|
|||
%span ##{pipeline_schedule.last_pipeline.id}
|
||||
- else
|
||||
= s_("PipelineSchedules|None")
|
||||
%td.next-run-cell
|
||||
%td.gl-text-gray-500{ 'data-testid': 'next-run-cell' }
|
||||
- if pipeline_schedule.active? && pipeline_schedule.next_run_at
|
||||
= time_ago_with_tooltip(pipeline_schedule.real_next_run)
|
||||
- else
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
Redis.raise_deprecations = true unless Rails.env.production?
|
||||
|
||||
# We set the instance variable directly to suppress warnings.
|
||||
# We cannot switch to the new behavior until we change all existing `redis.exists` calls to `redis.exists?`.
|
||||
# Some gems also need to be updated
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ScheduleRemoveSelfManagedWikiNotes < Gitlab::Database::Migration[2.0]
|
||||
restrict_gitlab_migration gitlab_schema: :gitlab_main
|
||||
|
||||
MIGRATION = 'RemoveSelfManagedWikiNotes'
|
||||
INTERVAL = 2.minutes
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
return if skip_migration?
|
||||
|
||||
queue_batched_background_migration(
|
||||
MIGRATION,
|
||||
:notes,
|
||||
:id,
|
||||
job_interval: INTERVAL,
|
||||
batch_size: 10_000,
|
||||
sub_batch_size: 1_000
|
||||
)
|
||||
end
|
||||
|
||||
def down
|
||||
return if skip_migration?
|
||||
|
||||
delete_batched_background_migration(MIGRATION, :notes, :id, [])
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def skip_migration?
|
||||
Gitlab.staging? || Gitlab.com?
|
||||
end
|
||||
end
|
|
@ -0,0 +1 @@
|
|||
9dc41d0d5f1c87f27327b254c955eada4fcc5c6158c513128e6fbdadd6c34932
|
|
@ -1363,7 +1363,10 @@ It renders on the GitLab documentation site as:
|
|||
|
||||
## Tabs
|
||||
|
||||
Use tabs to show different options.
|
||||
On the docs site, you can format text so it's displayed as tabs.
|
||||
|
||||
NOTE:
|
||||
For now, tabs are for testing only. Do not use them on the production docs site.
|
||||
|
||||
To create a set of tabs, follow this example:
|
||||
|
||||
|
@ -1389,7 +1392,7 @@ The headings determine the tab titles. Each tab is populated with the content be
|
|||
|
||||
Use brief words for the titles, ensure they are parallel, and start each with a capital letter. For example:
|
||||
|
||||
- `Omnibus`, `Chart`, `Source`
|
||||
- `Omnibus package`, `Helm chart`, `Source`
|
||||
- `15.1 and earlier`, `15.2 and later`
|
||||
|
||||
See [Pajamas](https://design.gitlab.com/components/tabs/#guidelines) for details.
|
||||
|
|
|
@ -205,7 +205,34 @@ To actually initialize this component, make sure to call the `initToggle` helper
|
|||
For the full list of options, see its
|
||||
[source](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/components/pajamas/toggle_component.rb).
|
||||
|
||||
### Best practices
|
||||
## Layouts
|
||||
|
||||
Layout components can be used to create common layout patterns used in GitLab.
|
||||
|
||||
### Available components
|
||||
|
||||
#### Horizontal section
|
||||
|
||||
Many of the settings pages use a layout where the title and description are on the left and the settings fields are on the right. The `Layouts::HorizontalSectionComponent` can be used to create this layout.
|
||||
|
||||
**Example:**
|
||||
|
||||
```haml
|
||||
= render ::Layouts::HorizontalSectionComponent.new(options: { class: 'gl-mb-6' }) do |c|
|
||||
= c.title { _('Naming, visibility') }
|
||||
= c.description do
|
||||
= _('Update your group name, description, avatar, and visibility.')
|
||||
= link_to _('Learn more about groups.'), help_page_path('user/group/index')
|
||||
= c.body do
|
||||
.form-group.gl-form-group
|
||||
= f.label :name, _('New group name')
|
||||
= f.text_field :name
|
||||
```
|
||||
|
||||
For the full list of options, see its
|
||||
[source](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/components/layouts/horizontal_section_component.rb).
|
||||
|
||||
## Best practices
|
||||
|
||||
- If you are about to create a new view in Haml, use the available components
|
||||
over creating plain Haml tags with CSS classes.
|
||||
|
|
|
@ -5,9 +5,9 @@ module Gitlab
|
|||
class IncrementPerAction < BaseStrategy
|
||||
def increment(cache_key, expiry)
|
||||
with_redis do |redis|
|
||||
redis.pipelined do
|
||||
redis.incr(cache_key)
|
||||
redis.expire(cache_key, expiry)
|
||||
redis.pipelined do |pipeline|
|
||||
pipeline.incr(cache_key)
|
||||
pipeline.expire(cache_key, expiry)
|
||||
end.first
|
||||
end
|
||||
end
|
||||
|
|
|
@ -9,10 +9,10 @@ module Gitlab
|
|||
|
||||
def increment(cache_key, expiry)
|
||||
with_redis do |redis|
|
||||
redis.pipelined do
|
||||
redis.sadd(cache_key, resource_key)
|
||||
redis.expire(cache_key, expiry)
|
||||
redis.scard(cache_key)
|
||||
redis.pipelined do |pipeline|
|
||||
pipeline.sadd(cache_key, resource_key)
|
||||
pipeline.expire(cache_key, expiry)
|
||||
pipeline.scard(cache_key)
|
||||
end.last
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module BackgroundMigration
|
||||
# Removes obsolete wiki notes
|
||||
class RemoveSelfManagedWikiNotes < BatchedMigrationJob
|
||||
def perform
|
||||
each_sub_batch(
|
||||
operation_name: :delete_all
|
||||
) do |sub_batch|
|
||||
sub_batch.where(noteable_type: 'Wiki').delete_all
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -153,11 +153,11 @@ module Gitlab
|
|||
# timeout - The time after which the cache key should expire.
|
||||
def self.write_multiple(mapping, key_prefix: nil, timeout: TIMEOUT)
|
||||
Redis::Cache.with do |redis|
|
||||
redis.pipelined do |multi|
|
||||
redis.pipelined do |pipeline|
|
||||
mapping.each do |raw_key, value|
|
||||
key = cache_key_for("#{key_prefix}#{raw_key}")
|
||||
|
||||
multi.set(key, value, ex: timeout)
|
||||
pipeline.set(key, value, ex: timeout)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -48,14 +48,14 @@ module Gitlab
|
|||
::Gitlab::Redis::Cache.with do |redis|
|
||||
# we use a pipeline instead of a MSET because each tag has
|
||||
# a specific ttl
|
||||
redis.pipelined do
|
||||
redis.pipelined do |pipeline|
|
||||
cacheable_tags.each do |tag|
|
||||
created_at = tag.created_at
|
||||
# ttl is the max_ttl_in_seconds reduced by the number
|
||||
# of seconds that the tag has already existed
|
||||
ttl = max_ttl_in_seconds - (now - created_at).seconds
|
||||
ttl = ttl.to_i
|
||||
redis.set(cache_key(tag), created_at.rfc3339, ex: ttl) if ttl > 0
|
||||
pipeline.set(cache_key(tag), created_at.rfc3339, ex: ttl) if ttl > 0
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -135,9 +135,9 @@ module Gitlab
|
|||
#
|
||||
def write_to_redis_hash(hash)
|
||||
Gitlab::Redis::Cache.with do |redis|
|
||||
redis.pipelined do
|
||||
redis.pipelined do |pipeline|
|
||||
hash.each do |diff_file_id, highlighted_diff_lines_hash|
|
||||
redis.hset(
|
||||
pipeline.hset(
|
||||
key,
|
||||
diff_file_id,
|
||||
gzip_compress(highlighted_diff_lines_hash.to_json)
|
||||
|
@ -147,7 +147,7 @@ module Gitlab
|
|||
end
|
||||
|
||||
# HSETs have to have their expiration date manually updated
|
||||
redis.expire(key, expiration)
|
||||
pipeline.expire(key, expiration)
|
||||
end
|
||||
|
||||
record_memory_usage(fetch_memory_usage(redis, key))
|
||||
|
@ -202,9 +202,9 @@ module Gitlab
|
|||
expiration_period = expiration
|
||||
|
||||
Gitlab::Redis::Cache.with do |redis|
|
||||
redis.pipelined do
|
||||
results = redis.hmget(cache_key, file_paths)
|
||||
redis.expire(key, expiration_period) if highlight_diffs_renewable_expiration_enabled
|
||||
redis.pipelined do |pipeline|
|
||||
results = pipeline.hmget(cache_key, file_paths)
|
||||
pipeline.expire(key, expiration_period) if highlight_diffs_renewable_expiration_enabled
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -16,9 +16,9 @@ module Gitlab
|
|||
etags = keys.map { generate_etag }
|
||||
|
||||
Gitlab::Redis::SharedState.with do |redis|
|
||||
redis.pipelined do
|
||||
redis.pipelined do |pipeline|
|
||||
keys.each_with_index do |key, i|
|
||||
redis.set(redis_shared_state_key(key), etags[i], ex: EXPIRY_TIME, nx: only_if_missing)
|
||||
pipeline.set(redis_shared_state_key(key), etags[i], ex: EXPIRY_TIME, nx: only_if_missing)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -20,8 +20,8 @@ module Gitlab
|
|||
|
||||
def store(new_access, new_reason, new_refreshed_at)
|
||||
::Gitlab::Redis::Cache.with do |redis|
|
||||
redis.pipelined do
|
||||
redis.mapped_hmset(
|
||||
redis.pipelined do |pipeline|
|
||||
pipeline.mapped_hmset(
|
||||
cache_key,
|
||||
{
|
||||
access: new_access.to_s,
|
||||
|
@ -30,7 +30,7 @@ module Gitlab
|
|||
}
|
||||
)
|
||||
|
||||
redis.expire(cache_key, VALIDITY_TIME)
|
||||
pipeline.expire(cache_key, VALIDITY_TIME)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -14,9 +14,9 @@ module Gitlab
|
|||
|
||||
def save(repositories, group_id)
|
||||
Gitlab::Redis::SharedState.with do |redis|
|
||||
redis.multi do
|
||||
redis.set(key_for('repositories'), Gitlab::Json.dump(repositories), ex: EXPIRY_TIME)
|
||||
redis.set(key_for('group_id'), group_id, ex: EXPIRY_TIME)
|
||||
redis.multi do |multi|
|
||||
multi.set(key_for('repositories'), Gitlab::Json.dump(repositories), ex: EXPIRY_TIME)
|
||||
multi.set(key_for('group_id'), group_id, ex: EXPIRY_TIME)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -10,9 +10,9 @@ module Gitlab
|
|||
results = {}
|
||||
|
||||
Gitlab::Redis::Cache.with do |r|
|
||||
r.pipelined do
|
||||
r.pipelined do |pipeline|
|
||||
subjects.each do |subject|
|
||||
results[subject.cache_key] = new(subject).read
|
||||
results[subject.cache_key] = new(subject).read(pipeline)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -34,11 +34,15 @@ module Gitlab
|
|||
end
|
||||
end
|
||||
|
||||
def read
|
||||
def read(pipeline = nil)
|
||||
@loaded = true
|
||||
|
||||
Gitlab::Redis::Cache.with do |r|
|
||||
r.mapped_hmget(markdown_cache_key, *fields)
|
||||
if pipeline
|
||||
pipeline.mapped_hmget(markdown_cache_key, *fields)
|
||||
else
|
||||
Gitlab::Redis::Cache.with do |r|
|
||||
r.mapped_hmget(markdown_cache_key, *fields)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -15,8 +15,8 @@ module Gitlab
|
|||
keys = read(key).map { |value| "#{cache_namespace}:#{value}" }
|
||||
keys << cache_key(key)
|
||||
|
||||
redis.pipelined do
|
||||
keys.each_slice(1000) { |subset| redis.unlink(*subset) }
|
||||
redis.pipelined do |pipeline|
|
||||
keys.each_slice(1000) { |subset| pipeline.unlink(*subset) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -267,7 +267,7 @@ module Gitlab
|
|||
|
||||
def same_redis_store?
|
||||
strong_memoize(:same_redis_store) do
|
||||
# <Redis client v4.4.0 for redis:///path_to/redis/redis.socket/5>"
|
||||
# <Redis client v4.7.1 for unix:///path_to/redis/redis.socket/5>"
|
||||
primary_store.inspect == secondary_store.inspect
|
||||
end
|
||||
end
|
||||
|
|
|
@ -83,14 +83,14 @@ module Gitlab
|
|||
full_key = cache_key(key)
|
||||
|
||||
with do |redis|
|
||||
results = redis.pipelined do
|
||||
results = redis.pipelined do |pipeline|
|
||||
# Set each hash key to the provided value
|
||||
hash.each do |h_key, h_value|
|
||||
redis.hset(full_key, h_key, h_value)
|
||||
pipeline.hset(full_key, h_key, h_value)
|
||||
end
|
||||
|
||||
# Update the expiry time for this hset
|
||||
redis.expire(full_key, expires_in)
|
||||
pipeline.expire(full_key, expires_in)
|
||||
end
|
||||
|
||||
results.all?
|
||||
|
|
|
@ -21,14 +21,14 @@ module Gitlab
|
|||
full_key = cache_key(key)
|
||||
|
||||
with do |redis|
|
||||
redis.multi do
|
||||
redis.unlink(full_key)
|
||||
redis.multi do |multi|
|
||||
multi.unlink(full_key)
|
||||
|
||||
# Splitting into groups of 1000 prevents us from creating a too-long
|
||||
# Redis command
|
||||
value.each_slice(1000) { |subset| redis.sadd(full_key, subset) }
|
||||
value.each_slice(1000) { |subset| multi.sadd(full_key, subset) }
|
||||
|
||||
redis.expire(full_key, expires_in)
|
||||
multi.expire(full_key, expires_in)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -39,9 +39,9 @@ module Gitlab
|
|||
full_key = cache_key(key)
|
||||
|
||||
smembers, exists = with do |redis|
|
||||
redis.multi do
|
||||
redis.smembers(full_key)
|
||||
redis.exists(full_key)
|
||||
redis.multi do |multi|
|
||||
multi.smembers(full_key)
|
||||
multi.exists(full_key)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -33,10 +33,10 @@ module Gitlab
|
|||
|
||||
def write(key, value)
|
||||
with do |redis|
|
||||
redis.pipelined do
|
||||
redis.sadd(cache_key(key), value)
|
||||
redis.pipelined do |pipeline|
|
||||
pipeline.sadd(cache_key(key), value)
|
||||
|
||||
redis.expire(cache_key(key), expires_in)
|
||||
pipeline.expire(cache_key(key), expires_in)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -57,9 +57,9 @@ module Gitlab
|
|||
full_key = cache_key(key)
|
||||
|
||||
with do |redis|
|
||||
redis.multi do
|
||||
redis.sismember(full_key, value)
|
||||
redis.exists(full_key)
|
||||
redis.multi do |multi|
|
||||
multi.sismember(full_key, value)
|
||||
multi.exists(full_key)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -40,9 +40,9 @@ module Gitlab
|
|||
cache_key = cache_key_for_hook(hook)
|
||||
|
||||
::Gitlab::Redis::SharedState.with do |redis|
|
||||
redis.multi do
|
||||
redis.sadd(cache_key, hook.id)
|
||||
redis.expire(cache_key, TOUCH_CACHE_TTL)
|
||||
redis.multi do |multi|
|
||||
multi.sadd(cache_key, hook.id)
|
||||
multi.expire(cache_key, TOUCH_CACHE_TTL)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -32349,6 +32349,9 @@ msgstr ""
|
|||
msgid "Register with two-factor app"
|
||||
msgstr ""
|
||||
|
||||
msgid "Register with:"
|
||||
msgstr ""
|
||||
|
||||
msgid "RegistrationFeatures|Enable Service Ping and register for this feature."
|
||||
msgstr ""
|
||||
|
||||
|
@ -36563,6 +36566,12 @@ msgstr ""
|
|||
msgid "Sign-up restrictions"
|
||||
msgstr ""
|
||||
|
||||
msgid "SignUp|By clicking %{button_text} or registering through a third party you accept the GitLab%{link_start} Terms of Use and acknowledge the Privacy Policy and Cookie Policy%{link_end}"
|
||||
msgstr ""
|
||||
|
||||
msgid "SignUp|By clicking %{button_text} or registering through a third party you accept the%{link_start} Terms of Use and acknowledge the Privacy Policy and Cookie Policy%{link_end}"
|
||||
msgstr ""
|
||||
|
||||
msgid "SignUp|By clicking %{button_text}, I agree that I have read and accepted the %{link_start}Terms of Use and Privacy Policy%{link_end}"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -52,7 +52,7 @@
|
|||
"@gitlab/at.js": "1.5.7",
|
||||
"@gitlab/favicon-overlay": "2.0.0",
|
||||
"@gitlab/svgs": "3.1.0",
|
||||
"@gitlab/ui": "43.6.0",
|
||||
"@gitlab/ui": "43.7.1",
|
||||
"@gitlab/visual-review-tools": "1.7.3",
|
||||
"@rails/actioncable": "6.1.4-7",
|
||||
"@rails/ujs": "6.1.4-7",
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
# frozen_string_literal: true
|
||||
require "spec_helper"
|
||||
|
||||
RSpec.describe Layouts::HorizontalSectionComponent, type: :component do
|
||||
let(:title) { 'Naming, visibility' }
|
||||
let(:description) { 'Update your group name, description, avatar, and visibility.' }
|
||||
let(:body) { 'This is where the settings go' }
|
||||
|
||||
describe 'slots' do
|
||||
it 'renders title' do
|
||||
render_inline described_class.new do |c|
|
||||
c.title { title }
|
||||
c.body { body }
|
||||
end
|
||||
|
||||
expect(page).to have_css('h4', text: title)
|
||||
end
|
||||
|
||||
it 'renders body slot' do
|
||||
render_inline described_class.new do |c|
|
||||
c.title { title }
|
||||
c.body { body }
|
||||
end
|
||||
|
||||
expect(page).to have_content(body)
|
||||
end
|
||||
|
||||
context 'when description slot is provided' do
|
||||
before do
|
||||
render_inline described_class.new do |c|
|
||||
c.title { title }
|
||||
c.description { description }
|
||||
c.body { body }
|
||||
end
|
||||
end
|
||||
|
||||
it 'renders description' do
|
||||
expect(page).to have_css('p', text: description)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when description slot is not provided' do
|
||||
before do
|
||||
render_inline described_class.new do |c|
|
||||
c.title { title }
|
||||
c.body { body }
|
||||
end
|
||||
end
|
||||
|
||||
it 'does not render description' do
|
||||
expect(page).not_to have_css('p', text: description)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'arguments' do
|
||||
describe 'border' do
|
||||
it 'defaults to true and adds gl-border-b CSS class' do
|
||||
render_inline described_class.new do |c|
|
||||
c.title { title }
|
||||
c.body { body }
|
||||
end
|
||||
|
||||
expect(page).to have_css('.gl-border-b')
|
||||
end
|
||||
|
||||
it 'does not add gl-border-b CSS class when set to false' do
|
||||
render_inline described_class.new(border: false) do |c|
|
||||
c.title { title }
|
||||
c.body { body }
|
||||
end
|
||||
|
||||
expect(page).not_to have_css('.gl-border-b')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'options' do
|
||||
it 'adds options to wrapping element' do
|
||||
render_inline described_class.new(options: { data: { testid: 'foo-bar' }, class: 'foo-bar' }) do |c|
|
||||
c.title { title }
|
||||
c.body { body }
|
||||
end
|
||||
|
||||
expect(page).to have_css('.foo-bar[data-testid="foo-bar"]')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,22 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Layouts
|
||||
class HorizontalSectionComponentPreview < ViewComponent::Preview
|
||||
# @param border toggle
|
||||
# @param title text
|
||||
# @param description text
|
||||
# @param body text
|
||||
def default(
|
||||
border: true,
|
||||
title: 'Naming, visibility',
|
||||
description: 'Update your group name, description, avatar, and visibility.',
|
||||
body: 'Settings fields here.'
|
||||
)
|
||||
render(::Layouts::HorizontalSectionComponent.new(border: border, options: { class: 'gl-mb-6 gl-pb-3' })) do |c|
|
||||
c.title { title }
|
||||
c.description { description }
|
||||
c.body { body }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -102,6 +102,21 @@ RSpec.describe 'Projects > Files > User edits files', :js do
|
|||
expect(page).to have_content('*.rbca')
|
||||
end
|
||||
|
||||
it 'shows loader on commit changes' do
|
||||
set_default_button('edit')
|
||||
click_link('.gitignore')
|
||||
click_link_or_button('Edit')
|
||||
|
||||
# why: We don't want the form to actually submit, so that we can assert the button's changed state
|
||||
page.execute_script("document.querySelector('.js-edit-blob-form').addEventListener('submit', e => e.preventDefault())")
|
||||
|
||||
find('.file-editor', match: :first)
|
||||
editor_set_value('*.rbca')
|
||||
click_button('Commit changes')
|
||||
|
||||
expect(page).to have_button('Commit changes', disabled: true, class: 'js-commit-button-loading')
|
||||
end
|
||||
|
||||
it 'shows the diff of an edited file' do
|
||||
set_default_button('edit')
|
||||
click_link('.gitignore')
|
||||
|
|
|
@ -95,7 +95,7 @@ RSpec.describe 'Pipeline Schedules', :js do
|
|||
it 'displays the required information description' do
|
||||
page.within('.pipeline-schedule-table-row') do
|
||||
expect(page).to have_content('pipeline schedule')
|
||||
expect(find(".next-run-cell time")['title'])
|
||||
expect(find("[data-testid='next-run-cell'] time")['title'])
|
||||
.to include(pipeline_schedule.real_next_run.strftime('%b %-d, %Y'))
|
||||
expect(page).to have_link('master')
|
||||
expect(page).to have_link("##{pipeline.id}")
|
||||
|
@ -259,7 +259,7 @@ RSpec.describe 'Pipeline Schedules', :js do
|
|||
click_button 'Save pipeline schedule'
|
||||
|
||||
page.within('.pipeline-schedule-table-row:nth-child(1)') do
|
||||
expect(page).to have_css(".next-run-cell time")
|
||||
expect(page).to have_css("[data-testid='next-run-cell'] time")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -329,6 +329,12 @@ describe('Pipeline New Form', () => {
|
|||
value: mockYmlValue,
|
||||
description: null,
|
||||
},
|
||||
yml_var2: {
|
||||
value: 'yml_var2_val',
|
||||
},
|
||||
yml_var3: {
|
||||
description: '',
|
||||
},
|
||||
});
|
||||
|
||||
await waitForPromises();
|
||||
|
|
|
@ -1,11 +1,17 @@
|
|||
import { GlIcon } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue';
|
||||
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
|
||||
|
||||
let wrapper;
|
||||
|
||||
function createComponent(propsData) {
|
||||
wrapper = shallowMount(WorkItemTypeIcon, { propsData });
|
||||
wrapper = shallowMount(WorkItemTypeIcon, {
|
||||
propsData,
|
||||
directives: {
|
||||
GlTooltip: createMockDirective(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
describe('Work Item type component', () => {
|
||||
|
@ -16,22 +22,23 @@ describe('Work Item type component', () => {
|
|||
});
|
||||
|
||||
describe.each`
|
||||
workItemType | workItemIconName | iconName | text
|
||||
${'TASK'} | ${''} | ${'issue-type-task'} | ${'Task'}
|
||||
${''} | ${'issue-type-task'} | ${'issue-type-task'} | ${''}
|
||||
${'ISSUE'} | ${''} | ${'issue-type-issue'} | ${'Issue'}
|
||||
${''} | ${'issue-type-issue'} | ${'issue-type-issue'} | ${''}
|
||||
${'REQUIREMENTS'} | ${''} | ${'issue-type-requirements'} | ${'Requirements'}
|
||||
${'INCIDENT'} | ${''} | ${'issue-type-incident'} | ${'Incident'}
|
||||
${'TEST_CASE'} | ${''} | ${'issue-type-test-case'} | ${'Test case'}
|
||||
${'random-issue-type'} | ${''} | ${'issue-type-issue'} | ${''}
|
||||
workItemType | workItemIconName | iconName | text | showTooltipOnHover
|
||||
${'TASK'} | ${''} | ${'issue-type-task'} | ${'Task'} | ${false}
|
||||
${''} | ${'issue-type-task'} | ${'issue-type-task'} | ${''} | ${true}
|
||||
${'ISSUE'} | ${''} | ${'issue-type-issue'} | ${'Issue'} | ${true}
|
||||
${''} | ${'issue-type-issue'} | ${'issue-type-issue'} | ${''} | ${true}
|
||||
${'REQUIREMENTS'} | ${''} | ${'issue-type-requirements'} | ${'Requirements'} | ${true}
|
||||
${'INCIDENT'} | ${''} | ${'issue-type-incident'} | ${'Incident'} | ${false}
|
||||
${'TEST_CASE'} | ${''} | ${'issue-type-test-case'} | ${'Test case'} | ${true}
|
||||
${'random-issue-type'} | ${''} | ${'issue-type-issue'} | ${''} | ${true}
|
||||
`(
|
||||
'with workItemType set to "$workItemType" and workItemIconName set to "$workItemIconName"',
|
||||
({ workItemType, workItemIconName, iconName, text }) => {
|
||||
({ workItemType, workItemIconName, iconName, text, showTooltipOnHover }) => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
workItemType,
|
||||
workItemIconName,
|
||||
showTooltipOnHover,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -42,6 +49,12 @@ describe('Work Item type component', () => {
|
|||
it(`renders correct text`, () => {
|
||||
expect(wrapper.text()).toBe(text);
|
||||
});
|
||||
|
||||
it('shows tooltip on hover when props passed', () => {
|
||||
const tooltip = getBinding(findIcon().element, 'gl-tooltip');
|
||||
|
||||
expect(tooltip.value).toBe(showTooltipOnHover);
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
@ -22,7 +22,7 @@ RSpec.describe 'ActionCableSubscriptionAdapterIdentifier override' do
|
|||
|
||||
sub = ActionCable.server.pubsub.send(:redis_connection)
|
||||
|
||||
expect(sub.connection[:id]).to eq('redis:///home/localuser/redis/redis.socket/0')
|
||||
expect(sub.connection[:id]).to eq('unix:///home/localuser/redis/redis.socket/0')
|
||||
expect(ActionCable.server.config.cable[:id]).to be_nil
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::BackgroundMigration::RemoveSelfManagedWikiNotes, :migration, schema: 20220601110011 do
|
||||
let(:notes) { table(:notes) }
|
||||
|
||||
subject(:perform_migration) do
|
||||
described_class.new(start_id: 1,
|
||||
end_id: 30,
|
||||
batch_table: :notes,
|
||||
batch_column: :id,
|
||||
sub_batch_size: 2,
|
||||
pause_ms: 0,
|
||||
connection: ActiveRecord::Base.connection)
|
||||
.perform
|
||||
end
|
||||
|
||||
it 'removes all wiki notes' do
|
||||
notes.create!(id: 2, note: 'Commit note', noteable_type: 'Commit')
|
||||
notes.create!(id: 10, note: 'Issue note', noteable_type: 'Issue')
|
||||
notes.create!(id: 20, note: 'Wiki note', noteable_type: 'Wiki')
|
||||
notes.create!(id: 30, note: 'MergeRequest note', noteable_type: 'MergeRequest')
|
||||
|
||||
expect(notes.where(noteable_type: 'Wiki').size).to eq(1)
|
||||
|
||||
expect { perform_migration }.to change(notes, :count).by(-1)
|
||||
|
||||
expect(notes.where(noteable_type: 'Wiki').size).to eq(0)
|
||||
end
|
||||
end
|
|
@ -79,10 +79,14 @@ RSpec.describe ::Gitlab::ContainerRepository::Tags::Cache, :clean_gitlab_redis_c
|
|||
|
||||
it 'inserts values in redis' do
|
||||
::Gitlab::Redis::Cache.with do |redis|
|
||||
expect(redis)
|
||||
.to receive(:set)
|
||||
.with(cache_key(tag), rfc3339(tag.created_at), ex: ttl.to_i)
|
||||
.and_call_original
|
||||
expect(redis).to receive(:pipelined).and_call_original
|
||||
|
||||
expect_next_instance_of(Redis::PipelinedConnection) do |pipeline|
|
||||
expect(pipeline)
|
||||
.to receive(:set)
|
||||
.with(cache_key(tag), rfc3339(tag.created_at), ex: ttl.to_i)
|
||||
.and_call_original
|
||||
end
|
||||
end
|
||||
|
||||
subject
|
||||
|
|
|
@ -132,20 +132,12 @@ RSpec.describe Gitlab::Diff::HighlightCache, :clean_gitlab_redis_cache do
|
|||
.once
|
||||
.and_call_original
|
||||
|
||||
Gitlab::Redis::Cache.with do |redis|
|
||||
expect(redis).to receive(:expire).with(cache.key, expiration_period).at_least(:once)
|
||||
end
|
||||
|
||||
2.times { cache.write_if_empty }
|
||||
end
|
||||
|
||||
it 'reads from cache once' do
|
||||
expect(cache).to receive(:read_cache).once.and_call_original
|
||||
|
||||
Gitlab::Redis::Cache.with do |redis|
|
||||
expect(redis).to receive(:expire).with(cache.key, expiration_period).at_least(:once)
|
||||
end
|
||||
|
||||
cache.write_if_empty
|
||||
end
|
||||
|
||||
|
|
|
@ -62,7 +62,13 @@ RSpec.describe Gitlab::MarkdownCache::Redis::Extension, :clean_gitlab_redis_cach
|
|||
|
||||
it 'does not preload the markdown twice' do
|
||||
expect(Gitlab::MarkdownCache::Redis::Store).to receive(:bulk_read).and_call_original
|
||||
expect(Gitlab::Redis::Cache).to receive(:with).twice.and_call_original
|
||||
Gitlab::Redis::Cache.with do |redis|
|
||||
expect(redis).to receive(:pipelined).and_call_original
|
||||
|
||||
expect_next_instance_of(Redis::PipelinedConnection) do |pipeline|
|
||||
expect(pipeline).to receive(:mapped_hmget).once.and_call_original
|
||||
end
|
||||
end
|
||||
|
||||
klass.preload_markdown_cache!([thing])
|
||||
|
||||
|
|
|
@ -46,7 +46,7 @@ RSpec.describe Gitlab::Redis::DuplicateJobs do
|
|||
|
||||
expect(redis_instance.primary_store.connection[:id]).to eq("redis://test-host:6379/99")
|
||||
expect(redis_instance.primary_store.connection[:namespace]).to be_nil
|
||||
expect(redis_instance.secondary_store.connection[:id]).to eq("redis:///path/to/redis.sock/0")
|
||||
expect(redis_instance.secondary_store.connection[:id]).to eq("unix:///path/to/redis.sock/0")
|
||||
expect(redis_instance.secondary_store.connection[:namespace]).to eq("resque:gitlab")
|
||||
|
||||
expect(redis_instance.instance_name).to eq('DuplicateJobs')
|
||||
|
|
|
@ -264,13 +264,20 @@ RSpec.describe Gitlab::Redis::MultiStore do
|
|||
|
||||
context 'when the command is executed within pipelined block' do
|
||||
subject do
|
||||
multi_store.pipelined do
|
||||
multi_store.send(name, *args)
|
||||
multi_store.pipelined do |pipeline|
|
||||
pipeline.send(name, *args)
|
||||
end
|
||||
end
|
||||
|
||||
it 'is executed only 1 time on primary instance' do
|
||||
expect(primary_store).to receive(name).with(*args).once
|
||||
it 'is executed only 1 time on primary and secondary instance' do
|
||||
expect(primary_store).to receive(:pipelined).and_call_original
|
||||
expect(secondary_store).to receive(:pipelined).and_call_original
|
||||
|
||||
2.times do
|
||||
expect_next_instance_of(Redis::PipelinedConnection) do |pipeline|
|
||||
expect(pipeline).to receive(name).with(*args).once.and_call_original
|
||||
end
|
||||
end
|
||||
|
||||
subject
|
||||
end
|
||||
|
@ -438,14 +445,21 @@ RSpec.describe Gitlab::Redis::MultiStore do
|
|||
|
||||
context 'when the command is executed within pipelined block' do
|
||||
subject do
|
||||
multi_store.pipelined do
|
||||
multi_store.send(name, *args)
|
||||
multi_store.pipelined do |pipeline|
|
||||
pipeline.send(name, *args)
|
||||
end
|
||||
end
|
||||
|
||||
it 'is executed only 1 time on each instance', :aggregate_errors do
|
||||
expect(primary_store).to receive(name).with(*expected_args).once
|
||||
expect(secondary_store).to receive(name).with(*expected_args).once
|
||||
expect(primary_store).to receive(:pipelined).and_call_original
|
||||
expect_next_instance_of(Redis::PipelinedConnection) do |pipeline|
|
||||
expect(pipeline).to receive(name).with(*expected_args).once.and_call_original
|
||||
end
|
||||
|
||||
expect(secondary_store).to receive(:pipelined).and_call_original
|
||||
expect_next_instance_of(Redis::PipelinedConnection) do |pipeline|
|
||||
expect(pipeline).to receive(name).with(*expected_args).once.and_call_original
|
||||
end
|
||||
|
||||
subject
|
||||
end
|
||||
|
@ -781,14 +795,20 @@ RSpec.describe Gitlab::Redis::MultiStore do
|
|||
|
||||
context 'when the command is executed within pipelined block' do
|
||||
subject do
|
||||
multi_store.pipelined do
|
||||
multi_store.incr(key)
|
||||
multi_store.pipelined do |pipeline|
|
||||
pipeline.incr(key)
|
||||
end
|
||||
end
|
||||
|
||||
it 'is executed only 1 time on each instance', :aggregate_errors do
|
||||
expect(primary_store).to receive(:incr).with(key).once
|
||||
expect(secondary_store).to receive(:incr).with(key).once
|
||||
expect(primary_store).to receive(:pipelined).once.and_call_original
|
||||
expect(secondary_store).to receive(:pipelined).once.and_call_original
|
||||
|
||||
2.times do
|
||||
expect_next_instance_of(Redis::PipelinedConnection) do |pipeline|
|
||||
expect(pipeline).to receive(:incr).with(key).once
|
||||
end
|
||||
end
|
||||
|
||||
subject
|
||||
end
|
||||
|
|
|
@ -42,7 +42,7 @@ RSpec.describe Gitlab::Redis::SidekiqStatus do
|
|||
expect(redis_instance).to be_instance_of(::Gitlab::Redis::MultiStore)
|
||||
|
||||
expect(redis_instance.primary_store.connection[:id]).to eq("redis://test-host:6379/99")
|
||||
expect(redis_instance.secondary_store.connection[:id]).to eq("redis:///path/to/redis.sock/0")
|
||||
expect(redis_instance.secondary_store.connection[:id]).to eq("unix:///path/to/redis.sock/0")
|
||||
|
||||
expect(redis_instance.instance_name).to eq('SidekiqStatus')
|
||||
end
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Sidebars::Projects::Menus::DeploymentsMenu do
|
||||
let_it_be(:project) { create(:project, :repository) }
|
||||
let_it_be(:project, reload: true) { create(:project, :repository) }
|
||||
|
||||
let(:user) { project.first_owner }
|
||||
let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project) }
|
||||
|
@ -37,6 +37,40 @@ RSpec.describe Sidebars::Projects::Menus::DeploymentsMenu do
|
|||
|
||||
specify { is_expected.to be_nil }
|
||||
end
|
||||
|
||||
describe 'when the feature is disabled' do
|
||||
before do
|
||||
project.update_attribute("#{item_id}_access_level", 'disabled')
|
||||
end
|
||||
|
||||
it { is_expected.to be_nil }
|
||||
end
|
||||
|
||||
describe 'when split_operations_visibility_permissions FF is disabled' do
|
||||
before do
|
||||
stub_feature_flags(split_operations_visibility_permissions: false)
|
||||
end
|
||||
|
||||
it { is_expected.not_to be_nil }
|
||||
|
||||
context 'and the feature is disabled' do
|
||||
before do
|
||||
project.update_attribute("#{item_id}_access_level", 'disabled')
|
||||
end
|
||||
|
||||
it { is_expected.not_to be_nil }
|
||||
end
|
||||
|
||||
context 'and operations is disabled' do
|
||||
before do
|
||||
project.update_attribute(:operations_access_level, 'disabled')
|
||||
end
|
||||
|
||||
it do
|
||||
is_expected.to be_nil if [:environments, :feature_flags].include?(item_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'Feature Flags' do
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require_migration!
|
||||
|
||||
RSpec.describe ScheduleRemoveSelfManagedWikiNotes do
|
||||
let_it_be(:batched_migration) { described_class::MIGRATION }
|
||||
|
||||
it 'schedules new batched migration' do
|
||||
reversible_migration do |migration|
|
||||
migration.before -> {
|
||||
expect(batched_migration).not_to have_scheduled_batched_migration
|
||||
}
|
||||
|
||||
migration.after -> {
|
||||
expect(batched_migration).to have_scheduled_batched_migration(
|
||||
table_name: :notes,
|
||||
column_name: :id,
|
||||
interval: described_class::INTERVAL
|
||||
)
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
context 'with com? or staging?' do
|
||||
before do
|
||||
allow(::Gitlab).to receive(:com?).and_return(true)
|
||||
allow(::Gitlab).to receive(:staging?).and_return(false)
|
||||
end
|
||||
|
||||
it 'does not schedule new batched migration' do
|
||||
reversible_migration do |migration|
|
||||
migration.before -> {
|
||||
expect(batched_migration).not_to have_scheduled_batched_migration
|
||||
}
|
||||
|
||||
migration.after -> {
|
||||
expect(batched_migration).not_to have_scheduled_batched_migration
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -395,11 +395,8 @@ RSpec.describe Projects::ContainerRepository::CleanupTagsService, :clean_gitlab_
|
|||
end
|
||||
|
||||
it 'caches the created_at values' do
|
||||
::Gitlab::Redis::Cache.with do |redis|
|
||||
expect_mget(redis, tags_and_created_ats.keys)
|
||||
|
||||
expect_set(redis, cacheable_tags)
|
||||
end
|
||||
expect_mget(tags_and_created_ats.keys)
|
||||
expect_set(cacheable_tags)
|
||||
|
||||
expect(subject).to include(cached_tags_count: 0)
|
||||
end
|
||||
|
@ -412,12 +409,10 @@ RSpec.describe Projects::ContainerRepository::CleanupTagsService, :clean_gitlab_
|
|||
end
|
||||
|
||||
it 'uses them' do
|
||||
::Gitlab::Redis::Cache.with do |redis|
|
||||
expect_mget(redis, tags_and_created_ats.keys)
|
||||
expect_mget(tags_and_created_ats.keys)
|
||||
|
||||
# because C is already in cache, it should not be cached again
|
||||
expect_set(redis, cacheable_tags.except('C'))
|
||||
end
|
||||
# because C is already in cache, it should not be cached again
|
||||
expect_set(cacheable_tags.except('C'))
|
||||
|
||||
# We will ping the container registry for all tags *except* for C because it's cached
|
||||
expect(ContainerRegistry::Blob).to receive(:new).with(repository, { "digest" => "sha256:configA" }).and_call_original
|
||||
|
@ -429,15 +424,27 @@ RSpec.describe Projects::ContainerRepository::CleanupTagsService, :clean_gitlab_
|
|||
end
|
||||
end
|
||||
|
||||
def expect_mget(redis, keys)
|
||||
expect(redis).to receive(:mget).with(keys.map(&method(:cache_key))).and_call_original
|
||||
def expect_mget(keys)
|
||||
Gitlab::Redis::Cache.with do |redis|
|
||||
expect(redis).to receive(:mget).with(keys.map(&method(:cache_key))).and_call_original
|
||||
end
|
||||
end
|
||||
|
||||
def expect_set(redis, tags)
|
||||
tags.each do |tag_name, created_at|
|
||||
def expect_set(tags)
|
||||
selected_tags = tags.map do |tag_name, created_at|
|
||||
ex = 1.day.seconds - (Time.zone.now - created_at).seconds
|
||||
if ex > 0
|
||||
expect(redis).to receive(:set).with(cache_key(tag_name), rfc3339(created_at), ex: ex.to_i)
|
||||
[tag_name, created_at, ex.to_i] if ex.positive?
|
||||
end.compact
|
||||
|
||||
return if selected_tags.count.zero?
|
||||
|
||||
Gitlab::Redis::Cache.with do |redis|
|
||||
expect(redis).to receive(:pipelined).and_call_original
|
||||
|
||||
expect_next_instance_of(Redis::PipelinedConnection) do |pipeline|
|
||||
selected_tags.each do |tag_name, created_at, ex|
|
||||
expect(pipeline).to receive(:set).with(cache_key(tag_name), rfc3339(created_at), ex: ex)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -507,7 +514,11 @@ RSpec.describe Projects::ContainerRepository::CleanupTagsService, :clean_gitlab_
|
|||
def expect_caching
|
||||
::Gitlab::Redis::Cache.with do |redis|
|
||||
expect(redis).to receive(:mget).and_call_original
|
||||
expect(redis).to receive(:set).and_call_original
|
||||
expect(redis).to receive(:pipelined).and_call_original
|
||||
|
||||
expect_next_instance_of(Redis::PipelinedConnection) do |pipeline|
|
||||
expect(pipeline).to receive(:set).and_call_original
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -120,7 +120,7 @@ module LoginHelpers
|
|||
def register_via(provider, uid, email, additional_info: {})
|
||||
mock_auth_hash(provider, uid, email, additional_info: additional_info)
|
||||
visit new_user_registration_path
|
||||
expect(page).to have_content('Create an account using')
|
||||
expect(page).to have_content('Create an account using').or(have_content('Register with'))
|
||||
|
||||
click_link_or_button "oauth-login-#{provider}"
|
||||
end
|
||||
|
|
|
@ -7,13 +7,15 @@ RSpec.describe 'devise/shared/_signup_box' do
|
|||
let(:terms_path) { '_terms_path_' }
|
||||
|
||||
let(:translation_com) do
|
||||
s_("SignUp|By clicking %{button_text}, I agree that I have read and accepted "\
|
||||
"the GitLab %{link_start}Terms of Use and Privacy Policy%{link_end}")
|
||||
s_("SignUp|By clicking %{button_text} or registering through a third party you "\
|
||||
"accept the GitLab%{link_start} Terms of Use and acknowledge the Privacy Policy "\
|
||||
"and Cookie Policy%{link_end}")
|
||||
end
|
||||
|
||||
let(:translation_non_com) do
|
||||
s_("SignUp|By clicking %{button_text}, I agree that I have read and accepted "\
|
||||
"the %{link_start}Terms of Use and Privacy Policy%{link_end}")
|
||||
s_("SignUp|By clicking %{button_text} or registering through a third party you "\
|
||||
"accept the%{link_start} Terms of Use and acknowledge the Privacy Policy and "\
|
||||
"Cookie Policy%{link_end}")
|
||||
end
|
||||
|
||||
before do
|
||||
|
|
|
@ -38,6 +38,19 @@ var (
|
|||
Help: "How many messages gitlab-workhorse has received in total on pubsub.",
|
||||
},
|
||||
)
|
||||
totalActions = promauto.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "gitlab_workhorse_keywatcher_actions_total",
|
||||
Help: "Counts of various keywatcher actions",
|
||||
},
|
||||
[]string{"action"},
|
||||
)
|
||||
receivedBytes = promauto.NewCounter(
|
||||
prometheus.CounterOpts{
|
||||
Name: "gitlab_workhorse_keywatcher_received_bytes_total",
|
||||
Help: "How many bytes of messages gitlab-workhorse has received in total on pubsub.",
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -50,6 +63,8 @@ type KeyChan struct {
|
|||
Chan chan string
|
||||
}
|
||||
|
||||
func countAction(action string) { totalActions.WithLabelValues(action).Add(1) }
|
||||
|
||||
func processInner(conn redis.Conn) error {
|
||||
defer conn.Close()
|
||||
psc := redis.PubSubConn{Conn: conn}
|
||||
|
@ -63,13 +78,13 @@ func processInner(conn redis.Conn) error {
|
|||
case redis.Message:
|
||||
totalMessages.Inc()
|
||||
dataStr := string(v.Data)
|
||||
receivedBytes.Add(float64(len(dataStr)))
|
||||
msg := strings.SplitN(dataStr, "=", 2)
|
||||
if len(msg) != 2 {
|
||||
log.WithError(fmt.Errorf("keywatcher: invalid notification: %q", dataStr)).Error()
|
||||
continue
|
||||
}
|
||||
key, value := msg[0], msg[1]
|
||||
notifyChanWatchers(key, value)
|
||||
notifyChanWatchers(msg[0], msg[1])
|
||||
case error:
|
||||
log.WithError(fmt.Errorf("keywatcher: pubsub receive: %v", v)).Error()
|
||||
// Intermittent error, return nil so that it doesn't wait before reconnect
|
||||
|
@ -131,37 +146,52 @@ func Shutdown() {
|
|||
func notifyChanWatchers(key, value string) {
|
||||
keyWatcherMutex.Lock()
|
||||
defer keyWatcherMutex.Unlock()
|
||||
if chanList, ok := keyWatcher[key]; ok {
|
||||
for _, c := range chanList {
|
||||
c <- value
|
||||
keyWatchers.Dec()
|
||||
}
|
||||
delete(keyWatcher, key)
|
||||
|
||||
chanList, ok := keyWatcher[key]
|
||||
if !ok {
|
||||
countAction("drop-message")
|
||||
return
|
||||
}
|
||||
|
||||
countAction("deliver-message")
|
||||
for _, c := range chanList {
|
||||
c <- value
|
||||
keyWatchers.Dec()
|
||||
}
|
||||
delete(keyWatcher, key)
|
||||
}
|
||||
|
||||
func addKeyChan(kc *KeyChan) {
|
||||
keyWatcherMutex.Lock()
|
||||
defer keyWatcherMutex.Unlock()
|
||||
|
||||
keyWatcher[kc.Key] = append(keyWatcher[kc.Key], kc.Chan)
|
||||
keyWatchers.Inc()
|
||||
if len(keyWatcher[kc.Key]) == 1 {
|
||||
countAction("create-subscription")
|
||||
}
|
||||
}
|
||||
|
||||
func delKeyChan(kc *KeyChan) {
|
||||
keyWatcherMutex.Lock()
|
||||
defer keyWatcherMutex.Unlock()
|
||||
if chans, ok := keyWatcher[kc.Key]; ok {
|
||||
for i, c := range chans {
|
||||
if kc.Chan == c {
|
||||
keyWatcher[kc.Key] = append(chans[:i], chans[i+1:]...)
|
||||
keyWatchers.Dec()
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(keyWatcher[kc.Key]) == 0 {
|
||||
delete(keyWatcher, kc.Key)
|
||||
|
||||
chans, ok := keyWatcher[kc.Key]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
for i, c := range chans {
|
||||
if kc.Chan == c {
|
||||
keyWatcher[kc.Key] = append(chans[:i], chans[i+1:]...)
|
||||
keyWatchers.Dec()
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(keyWatcher[kc.Key]) == 0 {
|
||||
delete(keyWatcher, kc.Key)
|
||||
countAction("delete-subscription")
|
||||
}
|
||||
}
|
||||
|
||||
// WatchKeyStatus is used to tell how WatchKey returned
|
||||
|
@ -211,7 +241,6 @@ func WatchKey(key, value string, timeout time.Duration) (WatchKeyStatus, error)
|
|||
return WatchKeyStatusNoChange, nil
|
||||
}
|
||||
return WatchKeyStatusSeenChange, nil
|
||||
|
||||
case <-time.After(timeout):
|
||||
return WatchKeyStatusTimeout, nil
|
||||
}
|
||||
|
|
|
@ -1056,10 +1056,10 @@
|
|||
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.1.0.tgz#0108498a17e2f79d16158015db0be764b406cc09"
|
||||
integrity sha512-kZ45VTQOgLdwQCLRSj7+aohF+6AUnAaoucR1CFY/6DPDLnNNGeflwsCLN0sFBKwx42HLxFfNwvDmKOMLdSQg5A==
|
||||
|
||||
"@gitlab/ui@43.6.0":
|
||||
version "43.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-43.6.0.tgz#95f85f405455aa95ee5b68b5072bcbe1bd22f375"
|
||||
integrity sha512-d0ebKUU7BBBCVoblEA6iWbD3BfR4t0YsDbC7UlN7nyR++MhojCmaML+L9MuDeJGpd0DWR0HLxAGWawtcdeZErw==
|
||||
"@gitlab/ui@43.7.1":
|
||||
version "43.7.1"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-43.7.1.tgz#0550d08ed3312650eb08df9294f25022fe574c64"
|
||||
integrity sha512-L7Rf+Y2YcsnYFVf95m+8Q2EHXYRh2mbxYCu5S94xoIVUYEBGtiZW7uRsjeEuXZjo1yD54K7e3x9BzBem1onrZw==
|
||||
dependencies:
|
||||
"@popperjs/core" "^2.11.2"
|
||||
bootstrap-vue "2.20.1"
|
||||
|
|
Loading…
Reference in New Issue