Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-06-16 06:08:59 +00:00
parent 1325f8cf2d
commit 06bcbc77e4
60 changed files with 1211 additions and 476 deletions

View File

@ -1 +1 @@
15.0.0
15.1.0

View File

@ -0,0 +1,6 @@
.gl-form-checkbox.custom-control.custom-checkbox
= form.check_box(method,
formatted_input_options,
checked_value,
unchecked_value)
= render_label_with_help_text

View File

@ -0,0 +1,56 @@
# frozen_string_literal: true
# Renders a Pajamas compliant checkbox element
# Must be used in an instance of `ActionView::Helpers::FormBuilder`
module Pajamas
class CheckboxComponent < Pajamas::Component
include Pajamas::Concerns::CheckboxRadioLabelWithHelpText
include Pajamas::Concerns::CheckboxRadioOptions
renders_one :label
renders_one :help_text
def initialize(
form:,
method:,
label: nil,
help_text: nil,
label_options: {},
checkbox_options: {},
checked_value: '1',
unchecked_value: '0'
)
@form = form
@method = method
@label_argument = label
@help_text_argument = help_text
@label_options = label_options
@input_options = checkbox_options
@checked_value = checked_value
@unchecked_value = unchecked_value
@value = checked_value if checkbox_options[:multiple]
end
attr_reader(
:form,
:method,
:label_argument,
:help_text_argument,
:label_options,
:input_options,
:checked_value,
:unchecked_value,
:value
)
private
def label_content
label? ? label : label_argument
end
def help_text_content
help_text? ? help_text : help_text_argument
end
end
end

View File

@ -16,5 +16,14 @@ module Pajamas
default
end
# Add CSS classes and additional options to an existing options hash
#
# @param [Hash] options
# @param [Array] css_classes
# @param [Hash] additional_option
def format_options(options:, css_classes: [], additional_options: {})
options.merge({ class: [*css_classes, options[:class]].flatten.compact }, additional_options)
end
end
end

View File

@ -0,0 +1,30 @@
# frozen_string_literal: true
module Pajamas
module Concerns
module CheckboxRadioLabelWithHelpText
def render_label_with_help_text
form.label(method, formatted_label_options) { label_entry }
end
private
def label_entry
if help_text_content
content_tag(:span, label_content) +
content_tag(:p, help_text_content, class: 'help-text', data: { testid: 'pajamas-component-help-text' })
else
content_tag(:span, label_content)
end
end
def formatted_label_options
format_options(
options: label_options,
css_classes: ['custom-control-label'],
additional_options: { value: value }
)
end
end
end
end

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
module Pajamas
module Concerns
module CheckboxRadioOptions
def formatted_input_options
format_options(options: input_options, css_classes: ['custom-control-input'])
end
end
end
end

View File

@ -0,0 +1,5 @@
.gl-form-radio.custom-control.custom-radio
= form.radio_button(method,
value,
formatted_input_options)
= render_label_with_help_text

View File

@ -0,0 +1,51 @@
# frozen_string_literal: true
# Renders a Pajamas compliant radio button element
# Must be used in an instance of `ActionView::Helpers::FormBuilder`
module Pajamas
class RadioComponent < Pajamas::Component
include Pajamas::Concerns::CheckboxRadioLabelWithHelpText
include Pajamas::Concerns::CheckboxRadioOptions
renders_one :label
renders_one :help_text
def initialize(
form:,
method:,
label: nil,
help_text: nil,
label_options: {},
radio_options: {},
value: nil
)
@form = form
@method = method
@label_argument = label
@help_text_argument = help_text
@label_options = label_options
@input_options = radio_options
@value = value
end
attr_reader(
:form,
:method,
:label_argument,
:help_text_argument,
:label_options,
:input_options,
:value
)
private
def label_content
label? ? label : label_argument
end
def help_text_content
help_text? ? help_text : help_text_argument
end
end
end

View File

@ -52,19 +52,11 @@ class Projects::ReleasesController < Projects::ApplicationController
end
def release
@release ||= project.releases.find_by_tag!(sanitized_tag_name)
@release ||= project.releases.find_by_tag!(params[:tag])
end
def link
release.links.find_by_filepath!(sanitized_filepath)
end
def sanitized_filepath
"/#{CGI.unescape(params[:filepath])}"
end
def sanitized_tag_name
CGI.unescape(params[:tag])
release.links.find_by_filepath!("/#{params[:filepath]}")
end
# Default order_by is 'released_at', which is set in ReleasesFinder.

View File

@ -225,6 +225,23 @@ module DiffHelper
end
end
def conflicts(allow_tree_conflicts: false)
return unless options[:merge_ref_head_diff]
conflicts_service = MergeRequests::Conflicts::ListService.new(merge_request, allow_tree_conflicts: allow_tree_conflicts) # rubocop:disable CodeReuse/ServiceClass
return unless allow_tree_conflicts || conflicts_service.can_be_resolved_in_ui?
conflicts_service.conflicts.files.index_by(&:path)
rescue Gitlab::Git::Conflict::Resolver::ConflictSideMissing
# This exception is raised when changes on a fork isn't present on canonical repo yet.
# We can't list conflicts until the canonical repo gets the references from the fork
# which happens asynchronously when updating MR.
#
# Return empty hash to indicate that there are no conflicts.
{}
end
private
def diff_btn(title, name, selected)
@ -271,16 +288,6 @@ module DiffHelper
Gitlab::CodeNavigationPath.new(merge_request.project, merge_request.diff_head_sha)
end
def conflicts(allow_tree_conflicts: false)
return unless options[:merge_ref_head_diff]
conflicts_service = MergeRequests::Conflicts::ListService.new(merge_request, allow_tree_conflicts: allow_tree_conflicts) # rubocop:disable CodeReuse/ServiceClass
return unless allow_tree_conflicts || conflicts_service.can_be_resolved_in_ui?
conflicts_service.conflicts.files.index_by(&:path)
end
def log_overflow_limits(diff_files:, collection_overflow:)
if diff_files.any?(&:too_large?)
Gitlab::Metrics.add_event(:diffs_overflow_single_file_limits)

View File

@ -1153,6 +1153,19 @@ class MergeRequest < ApplicationRecord
can_be_merged? && !should_be_rebased?
end
def mergeability_checks
# We want to have the cheapest checks first in the list, that way we can
# fail fast before running the more expensive ones.
#
[
::MergeRequests::Mergeability::CheckOpenStatusService,
::MergeRequests::Mergeability::CheckDraftStatusService,
::MergeRequests::Mergeability::CheckBrokenStatusService,
::MergeRequests::Mergeability::CheckDiscussionsStatusService,
::MergeRequests::Mergeability::CheckCiStatusService
]
end
# rubocop: disable CodeReuse/ServiceClass
def mergeable_state?(skip_ci_check: false, skip_discussions_check: false)
if Feature.enabled?(:improved_mergeability_checks, self.project)

View File

@ -54,7 +54,7 @@ class Release < ApplicationRecord
MAX_NUMBER_TO_DISPLAY = 3
def to_param
CGI.escape(tag)
tag
end
def commit

View File

@ -4,23 +4,13 @@ module MergeRequests
class RunChecksService
include Gitlab::Utils::StrongMemoize
# We want to have the cheapest checks first in the list,
# that way we can fail fast before running the more expensive ones
CHECKS = [
CheckOpenStatusService,
CheckDraftStatusService,
CheckBrokenStatusService,
CheckDiscussionsStatusService,
CheckCiStatusService
].freeze
def initialize(merge_request:, params:)
@merge_request = merge_request
@params = params
end
def execute
CHECKS.each_with_object([]) do |check_class, results|
merge_request.mergeability_checks.each_with_object([]) do |check_class, results|
check = check_class.new(merge_request: merge_request, params: params)
next if check.skip?

View File

@ -2,12 +2,12 @@
.col-sm-2.col-form-label.pt-0
= f.label :lfs_enabled, _('Large File Storage')
.col-sm-10
- label = _('Allow projects within this group to use Git LFS')
- help_link = link_to sprite_icon('question-o'), help_page_path('topics/git/lfs/index'), class: 'gl-ml-2'
= f.gitlab_ui_checkbox_component :lfs_enabled,
'%{label}%{help_link}'.html_safe % { label: label, help_link: help_link },
help_text: _('This setting can be overridden in each project.'),
checkbox_options: { checked: @group.lfs_enabled? }
= f.gitlab_ui_checkbox_component :lfs_enabled, checkbox_options: { checked: @group.lfs_enabled? } do |c|
= c.label do
= _('Allow projects within this group to use Git LFS')
= link_to sprite_icon('question-o'), help_page_path('topics/git/lfs/index'), class: 'gl-ml-2'
= c.help_text do
= _('This setting can be overridden in each project.')
.form-group.row
.col-sm-2.col-form-label
= f.label s_('ProjectCreationLevel|Allowed to create projects')

View File

@ -241,7 +241,9 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end
end
resources :releases, only: [:index, :new, :show, :edit], param: :tag, constraints: { tag: %r{[^/]+} } do
get 'releases/permalink/latest(/)(*suffix_path)', to: 'releases#latest_permalink', as: :latest_release_permalink, format: false
resources :releases, only: [:index, :new, :show, :edit], param: :tag, constraints: { tag: %r{[^\\]+} } do
member do
get :downloads, path: 'downloads/*filepath', format: false
scope module: :releases do
@ -250,8 +252,6 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end
end
get 'releases/permalink/latest(/)(*suffix_path)', to: 'releases#latest_permalink', as: :latest_release_permalink, format: false
resources :logs, only: [:index] do
collection do
get :k8s

View File

@ -559,8 +559,8 @@ Returns [`Vulnerability`](#vulnerability).
Find a work item. Returns `null` if `work_items` feature flag is disabled.
WARNING:
**Deprecated** in 15.1.
This feature is in Alpha, and can be removed or changed at any point.
**Introduced** in 15.1.
This feature is in Alpha. It can be changed or removed at any time.
Returns [`WorkItem`](#workitem).
@ -5413,8 +5413,8 @@ Input type: `VulnerabilityRevertToDetectedInput`
Creates a work item. Available only when feature flag `work_items` is enabled.
WARNING:
**Deprecated** in 15.1.
This feature is in Alpha, and can be removed or changed at any point.
**Introduced** in 15.1.
This feature is in Alpha. It can be changed or removed at any time.
Input type: `WorkItemCreateInput`
@ -5441,8 +5441,8 @@ Input type: `WorkItemCreateInput`
Creates a work item from a task in another work item's description. Available only when feature flag `work_items` is enabled.
WARNING:
**Deprecated** in 15.1.
This feature is in Alpha, and can be removed or changed at any point.
**Introduced** in 15.1.
This feature is in Alpha. It can be changed or removed at any time.
Input type: `WorkItemCreateFromTaskInput`
@ -5468,8 +5468,8 @@ Input type: `WorkItemCreateFromTaskInput`
Deletes a work item. Available only when feature flag `work_items` is enabled.
WARNING:
**Deprecated** in 15.1.
This feature is in Alpha, and can be removed or changed at any point.
**Introduced** in 15.1.
This feature is in Alpha. It can be changed or removed at any time.
Input type: `WorkItemDeleteInput`
@ -5493,8 +5493,8 @@ Input type: `WorkItemDeleteInput`
Deletes a task in a work item's description. Available only when feature flag `work_items` is enabled.
WARNING:
**Deprecated** in 15.1.
This feature is in Alpha, and can be removed or changed at any point.
**Introduced** in 15.1.
This feature is in Alpha. It can be changed or removed at any time.
Input type: `WorkItemDeleteTaskInput`
@ -5520,8 +5520,8 @@ Input type: `WorkItemDeleteTaskInput`
Updates a work item by Global ID. Available only when feature flag `work_items` is enabled.
WARNING:
**Deprecated** in 15.1.
This feature is in Alpha, and can be removed or changed at any point.
**Introduced** in 15.1.
This feature is in Alpha. It can be changed or removed at any time.
Input type: `WorkItemUpdateInput`
@ -5547,8 +5547,8 @@ Input type: `WorkItemUpdateInput`
Updates a work item's task by Global ID. Available only when feature flag `work_items` is enabled.
WARNING:
**Deprecated** in 15.1.
This feature is in Alpha, and can be removed or changed at any point.
**Introduced** in 15.1.
This feature is in Alpha. It can be changed or removed at any time.
Input type: `WorkItemUpdateTaskInput`
@ -5574,8 +5574,8 @@ Input type: `WorkItemUpdateTaskInput`
Updates the attributes of a work item's widgets by global ID. Available only when feature flag `work_items` is enabled.
WARNING:
**Deprecated** in 15.1.
This feature is in Alpha, and can be removed or changed at any point.
**Introduced** in 15.1.
This feature is in Alpha. It can be changed or removed at any time.
Input type: `WorkItemUpdateWidgetsInput`
@ -9806,7 +9806,7 @@ Represents the total number of issues and their weights for a particular day.
| <a id="cirunnershortsha"></a>`shortSha` | [`String`](#string) | First eight characters of the runner's token used to authenticate new job requests. Used as the runner's unique ID. |
| <a id="cirunnertaglist"></a>`tagList` | [`[String!]`](#string) | Tags associated with the runner. |
| <a id="cirunnertokenexpiresat"></a>`tokenExpiresAt` | [`Time`](#time) | Runner token expiration time. |
| <a id="cirunnerupgradestatus"></a>`upgradeStatus` **{warning-solid}** | [`CiRunnerUpgradeStatusType`](#cirunnerupgradestatustype) | **Deprecated** in 14.10. This feature is in Alpha, and can be removed or changed at any point. |
| <a id="cirunnerupgradestatus"></a>`upgradeStatus` **{warning-solid}** | [`CiRunnerUpgradeStatusType`](#cirunnerupgradestatustype) | **Introduced** in 14.10. This feature is in Alpha. It can be changed or removed at any time. |
| <a id="cirunneruserpermissions"></a>`userPermissions` | [`RunnerPermissions!`](#runnerpermissions) | Permissions for the current user on the resource. |
| <a id="cirunnerversion"></a>`version` | [`String`](#string) | Version of the runner. |
@ -15894,8 +15894,8 @@ four standard [pagination arguments](#connection-pagination-arguments):
Work items of the project.
WARNING:
**Deprecated** in 15.1.
This feature is in Alpha, and can be removed or changed at any point.
**Introduced** in 15.1.
This feature is in Alpha. It can be changed or removed at any time.
Returns [`WorkItemConnection`](#workitemconnection).

View File

@ -94,11 +94,16 @@ discussed in [Nullable fields](#nullable-fields).
- Lowering the global limits for query complexity and depth.
- Anything else that can result in queries hitting a limit that previously was allowed.
Fields that use the [`feature_flag` property](#feature_flag-property) and the flag is disabled by default are exempt
from the deprecation process, and can be removed at any time without notice.
See the [deprecating schema items](#deprecating-schema-items) section for how to deprecate items.
### Breaking change exemptions
Two scenarios exist where schema items are exempt from the deprecation process,
and can be removed or changed at any time without notice. These are schema items that either:
- Use the [`feature_flag` property](#feature_flag-property) _and_ the flag is disabled by default.
- Are [marked as alpha](#marking-schema-items-as-alpha).
## Global IDs
The GitLab GraphQL API uses Global IDs (i.e: `"gid://gitlab/MyObject/123"`)
@ -718,6 +723,28 @@ aware of the support.
The documentation will mention that the old Global ID style is now deprecated.
## Marking schema items as Alpha
Fields, arguments, enum values, and mutations can be marked as being in
[alpha](https://about.gitlab.com/handbook/product/gitlab-the-product/#alpha-beta-ga).
An item marked as "alpha" is exempt from the deprecation process and can be removed
at any time without notice.
This leverages GraphQL deprecations to cause the schema item to appear as deprecated,
and will be described as being in "alpha" in our generated docs and its GraphQL description.
To mark a schema item as being in "alpha", use the `deprecated:` keyword with `reason: :alpha`.
You must provide the `milestone:` that introduced the alpha item.
For example:
```ruby
field :token, GraphQL::Types::String, null: true,
deprecated: { reason: :alpha, milestone: '10.0' },
description: 'Token for login.'
```
## Enums
GitLab GraphQL enums are defined in `app/graphql/types`. When defining new enums, the

View File

@ -39,6 +39,15 @@ For example:
%span
= s_('GroupSettings|Prevent members from sending invitations to groups outside of %{group} and its subgroups.').html_safe % { group: link_to_group(@group) }
%p.help-text= prevent_sharing_groups_outside_hierarchy_help_text(@group)
.form-group.gl-mb-3
.gl-form-checkbox.custom-control.custom-checkbox
= f.check_box :lfs_enabled, checked: @group.lfs_enabled?, class: 'custom-control-input'
= f.label :lfs_enabled, class: 'custom-control-label' do
%span
= _('Allow projects within this group to use Git LFS')
= link_to sprite_icon('question-o'), help_page_path('topics/git/lfs/index')
%p.help-text= _('This setting can be overridden in each project.')
```
- After:
@ -50,6 +59,14 @@ For example:
s_('GroupSettings|Prevent members from sending invitations to groups outside of %{group} and its subgroups.').html_safe % { group: link_to_group(@group) },
help_text: prevent_sharing_groups_outside_hierarchy_help_text(@group),
checkbox_options: { disabled: !can_change_prevent_sharing_groups_outside_hierarchy?(@group) }
.form-group.gl-mb-3
= f.gitlab_ui_checkbox_component :lfs_enabled, checkbox_options: { checked: @group.lfs_enabled? } do |c|
= c.label do
= _('Allow projects within this group to use Git LFS')
= link_to sprite_icon('question-o'), help_page_path('topics/git/lfs/index')
= c.help_text do
= _('This setting can be overridden in each project.')
```
### Available components
@ -67,16 +84,27 @@ Currently only the listed components are available but more components are plann
[GitLab UI Docs](https://gitlab-org.gitlab.io/gitlab-ui/?path=/story/base-form-form-checkbox--default)
##### Arguments
| Argument | Description | Type | Required (default value) |
|---|---|---|---|
| `method` | Attribute on the object passed to `gitlab_ui_form_for`. | `Symbol` | `true` |
| `label` | Checkbox label. | `String` | `true` |
| `help_text` | Help text displayed below the checkbox. | `String` | `false` (`nil`) |
| `label` | Checkbox label. `label` slot can be used instead of this argument if HTML is needed. | `String` | `false` (`nil`) |
| `help_text` | Help text displayed below the checkbox. `help_text` slot can be used instead of this argument if HTML is needed. | `String` | `false` (`nil`) |
| `checkbox_options` | Options that are passed to [Rails `check_box` method](https://api.rubyonrails.org/classes/ActionView/Helpers/FormBuilder.html#method-i-check_box). | `Hash` | `false` (`{}`) |
| `checked_value` | Value when checkbox is checked. | `String` | `false` (`'1'`) |
| `unchecked_value` | Value when checkbox is unchecked. | `String` | `false` (`'0'`) |
| `label_options` | Options that are passed to [Rails `label` method](https://api.rubyonrails.org/classes/ActionView/Helpers/FormBuilder.html#method-i-label). | `Hash` | `false` (`{}`) |
##### Slots
This component supports [ViewComponent slots](https://viewcomponent.org/guide/slots.html).
| Slot | Description
|---|---|
| `label` | Checkbox label content. This slot can be used instead of the `label` argument. |
| `help_text` | Help text content displayed below the checkbox. This slot can be used instead of the `help_text` argument. |
<!-- vale gitlab.Spelling = NO -->
#### gitlab_ui_radio_component
@ -85,11 +113,22 @@ Currently only the listed components are available but more components are plann
[GitLab UI Docs](https://gitlab-org.gitlab.io/gitlab-ui/?path=/story/base-form-form-radio--default)
##### Arguments
| Argument | Description | Type | Required (default value) |
|---|---|---|---|
| `method` | Attribute on the object passed to `gitlab_ui_form_for`. | `Symbol` | `true` |
| `value` | The value of the radio tag. | `Symbol` | `true` |
| `label` | Radio label. | `String` | `true` |
| `help_text` | Help text displayed below the radio button. | `String` | `false` (`nil`) |
| `label` | Radio label. `label` slot can be used instead of this argument if HTML content is needed inside the label. | `String` | `false` (`nil`) |
| `help_text` | Help text displayed below the radio button. `help_text` slot can be used instead of this argument if HTML content is needed inside the help text. | `String` | `false` (`nil`) |
| `radio_options` | Options that are passed to [Rails `radio_button` method](https://api.rubyonrails.org/classes/ActionView/Helpers/FormBuilder.html#method-i-radio_button). | `Hash` | `false` (`{}`) |
| `label_options` | Options that are passed to [Rails `label` method](https://api.rubyonrails.org/classes/ActionView/Helpers/FormBuilder.html#method-i-label). | `Hash` | `false` (`{}`) |
##### Slots
This component supports [ViewComponent slots](https://viewcomponent.org/guide/slots.html).
| Slot | Description
|---|---|
| `label` | Checkbox label content. This slot can be used instead of the `label` argument. |
| `help_text` | Help text content displayed below the radio button. This slot can be used instead of the `help_text` argument. |

View File

@ -95,11 +95,9 @@ can still view the groups and their entities (like epics).
Project membership (where the group membership is already taken into account)
is stored in the `project_authorizations` table.
WARNING:
Due to [an issue](https://gitlab.com/gitlab-org/gitlab/-/issues/219299),
projects in personal namespace do not show owner (`50`) permission in
`project_authorizations` table. Note however that [`user.owned_projects`](https://gitlab.com/gitlab-org/gitlab/-/blob/0d63823b122b11abd2492bca47cc26858eee713d/app/models/user.rb#L906-916)
is calculated properly.
NOTE:
In [GitLab 14.9](https://gitlab.com/gitlab-org/gitlab/-/issues/351211) and later, projects in personal namespaces have a maximum role of Owner.
Because of a [known issue](https://gitlab.com/gitlab-org/gitlab/-/issues/219299) in GitLab 14.8 and earlier, projects in personal namespaces have a maximum role of Maintainer.
### Confidential issues

View File

@ -697,6 +697,10 @@ rules:
Vulnerabilities that have been detected and are false positives will be flagged as false positives in the security dashboard.
False positive detection is available in a subset of the [supported languages](#supported-languages-and-frameworks) and [analyzers](analyzers.md):
- Ruby, in the Brakeman-based analyzer
### Advanced vulnerability tracking **(ULTIMATE)**
> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/5144) in GitLab 14.2.
@ -715,7 +719,7 @@ Advanced vulnerability tracking is available in a subset of the [supported langu
- Java, in the Semgrep-based analyzer only
- JavaScript, in the Semgrep-based analyzer only
- Python, in the Semgrep-based analyzer only
- Ruby, in the Brakeman-based analyzers
- Ruby, in the Brakeman-based analyzer
Support for more languages and analyzers is tracked in [this epic](https://gitlab.com/groups/gitlab-org/-/epics/5144).

View File

@ -10,11 +10,22 @@ module API
content_type Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE
end
helpers do
def request_authenticated?
authenticator = Gitlab::Auth::RequestAuthenticator.new(request)
return true if authenticator.find_authenticated_requester([:api])
# Look up user from warden, ignoring the absence of a CSRF token. For
# web users the CSRF token can be in the POST form data but Workhorse
# does not propagate the form data to us.
!!request.env['warden']&.authenticate
end
end
namespace 'internal' do
namespace 'workhorse' do
post 'authorize_upload' do
authenticator = Gitlab::Auth::RequestAuthenticator.new(request)
unauthorized! unless authenticator.find_authenticated_requester([:api])
unauthorized! unless request_authenticated?
status 200
{ TempPath: File.join(::Gitlab.config.uploads.storage_path, 'uploads/tmp') }

View File

@ -5,76 +5,50 @@ module Gitlab
class GitlabUiFormBuilder < ActionView::Helpers::FormBuilder
def gitlab_ui_checkbox_component(
method,
label,
label = nil,
help_text: nil,
checkbox_options: {},
checked_value: '1',
unchecked_value: '0',
label_options: {}
label_options: {},
&block
)
@template.content_tag(
:div,
class: 'gl-form-checkbox custom-control custom-checkbox'
) do
value = checkbox_options[:multiple] ? checked_value : nil
@template.check_box(
@object_name,
method,
format_options(checkbox_options, ['custom-control-input']),
checked_value,
unchecked_value
) + generic_label(method, label, label_options, help_text: help_text, value: value)
end
Pajamas::CheckboxComponent.new(
form: self,
method: method,
label: label,
help_text: help_text,
checkbox_options: format_options(checkbox_options),
checked_value: checked_value,
unchecked_value: unchecked_value,
label_options: format_options(label_options)
).render_in(@template, &block)
end
def gitlab_ui_radio_component(
method,
value,
label,
label = nil,
help_text: nil,
radio_options: {},
label_options: {}
label_options: {},
&block
)
@template.content_tag(
:div,
class: 'gl-form-radio custom-control custom-radio'
) do
@template.radio_button(
@object_name,
method,
value,
format_options(radio_options, ['custom-control-input'])
) + generic_label(method, label, label_options, help_text: help_text, value: value)
end
Pajamas::RadioComponent.new(
form: self,
method: method,
value: value,
label: label,
help_text: help_text,
radio_options: format_options(radio_options),
label_options: format_options(label_options)
).render_in(@template, &block)
end
private
def generic_label(method, label, label_options, help_text: nil, value: nil)
@template.label(
@object_name, method, format_options(label_options.merge({ value: value }), ['custom-control-label'])
) do
if help_text
@template.content_tag(
:span,
label
) +
@template.content_tag(
:p,
help_text,
class: 'help-text'
)
else
label
end
end
end
def format_options(options, classes)
classes << options[:class]
objectify_options(options.merge({ class: classes.flatten.compact }))
def format_options(options)
objectify_options(options)
end
end
end

View File

@ -3,9 +3,12 @@
module Gitlab
module Graphql
class Deprecation
REASON_RENAMED = :renamed
REASON_ALPHA = :alpha
REASONS = {
renamed: 'This was renamed.',
alpha: 'This feature is in Alpha, and can be removed or changed at any point.'
REASON_RENAMED => 'This was renamed.',
REASON_ALPHA => 'This feature is in Alpha. It can be changed or removed at any time.'
}.freeze
include ActiveModel::Validations
@ -39,7 +42,7 @@ module Gitlab
def markdown(context: :inline)
parts = [
"#{deprecated_in(format: :markdown)}.",
"#{changed_in_milestone(format: :markdown)}.",
reason_text,
replacement_markdown.then { |r| "Use: #{r}." if r }
].compact
@ -77,7 +80,7 @@ module Gitlab
[
reason_text,
replacement && "Please use `#{replacement}`.",
"#{deprecated_in}."
"#{changed_in_milestone}."
].compact.join(' ')
end
@ -107,15 +110,24 @@ module Gitlab
end
def description_suffix
" #{deprecated_in}: #{reason_text}"
" #{changed_in_milestone}: #{reason_text}"
end
def deprecated_in(format: :plain)
# Returns 'Deprecated in <milestone>' for proper deprecations.
# Retruns 'Introduced in <milestone>' for :alpha deprecations.
# Formatted to markdown or plain format.
def changed_in_milestone(format: :plain)
verb = if reason == REASON_ALPHA
'Introduced'
else
'Deprecated'
end
case format
when :plain
"Deprecated in #{milestone}"
"#{verb} in #{milestone}"
when :markdown
"**Deprecated** in #{milestone}"
"**#{verb}** in #{milestone}"
end
end
end

View File

@ -10,24 +10,25 @@ provider = File.expand_path('provider', contracts)
# rubocop:disable Rails/RakeEnvironment
namespace :contracts do
namespace :mr do
Pact::VerificationTask.new(:metadata) do |pact|
Pact::VerificationTask.new(:diffs_batch) do |pact|
pact.uri(
"#{contracts}/contracts/merge_request_page-merge_request_metadata_endpoint.json",
pact_helper: "#{provider}/specs/metadata_helper.rb"
"#{contracts}/contracts/project/merge_request/show/mergerequest#show-merge_request_diffs_batch_endpoint.json",
pact_helper: "#{provider}/pact_helpers/project/merge_request/diffs_batch_helper.rb"
)
end
Pact::VerificationTask.new(:diffs_metadata) do |pact|
pact.uri(
"#{contracts}/contracts/project/merge_request/show/" \
"mergerequest#show-merge_request_diffs_metadata_endpoint.json",
pact_helper: "#{provider}/pact_helpers/project/merge_request/diffs_metadata_helper.rb"
)
end
Pact::VerificationTask.new(:discussions) do |pact|
pact.uri(
"#{contracts}/contracts/merge_request_page-merge_request_discussions_endpoint.json",
pact_helper: "#{provider}/specs/discussions_helper.rb"
)
end
Pact::VerificationTask.new(:diffs) do |pact|
pact.uri(
"#{contracts}/contracts/merge_request_page-merge_request_diffs_endpoint.json",
pact_helper: "#{provider}/specs/diffs_helper.rb"
"#{contracts}/contracts/project/merge_request/show/mergerequest#show-merge_request_discussions_endpoint.json",
pact_helper: "#{provider}/pact_helpers/project/merge_request/discussions_helper.rb"
)
end

View File

@ -0,0 +1,130 @@
# frozen_string_literal: true
require "spec_helper"
RSpec.describe Pajamas::CheckboxComponent, :aggregate_failures, type: :component do
include FormBuilderHelpers
let_it_be(:method) { :view_diffs_file_by_file }
let_it_be(:label) { "Show one file at a time on merge request's Changes tab" }
let_it_be(:help_text) { 'Instead of all the files changed, show only one file at a time.' }
RSpec.shared_examples 'it renders unchecked checkbox with value of `1`' do
it 'renders unchecked checkbox with value of `1`' do
expect(rendered_component).to have_unchecked_field(label, with: '1')
end
end
context 'with default options' do
before do
fake_form_for do |form|
render_inline(
described_class.new(
form: form,
method: method,
label: label
)
)
end
end
include_examples 'it renders unchecked checkbox with value of `1`'
include_examples 'it does not render help text'
it 'renders hidden input with value of `0`' do
expect(rendered_component).to have_field('user[view_diffs_file_by_file]', type: 'hidden', with: '0')
end
end
context 'with custom options' do
let_it_be(:checked_value) { 'yes' }
let_it_be(:unchecked_value) { 'no' }
let_it_be(:checkbox_options) { { class: 'checkbox-foo-bar', checked: true } }
let_it_be(:label_options) { { class: 'label-foo-bar' } }
before do
fake_form_for do |form|
render_inline(
described_class.new(
form: form,
method: method,
label: label,
help_text: help_text,
checked_value: checked_value,
unchecked_value: unchecked_value,
checkbox_options: checkbox_options,
label_options: label_options
)
)
end
end
include_examples 'it renders help text'
it 'renders checked checkbox with value of `yes`' do
expect(rendered_component).to have_checked_field(label, with: checked_value, class: checkbox_options[:class])
end
it 'adds CSS class to label' do
expect(rendered_component).to have_selector('label.label-foo-bar')
end
it 'renders hidden input with value of `no`' do
expect(rendered_component).to have_field('user[view_diffs_file_by_file]', type: 'hidden', with: unchecked_value)
end
end
context 'with `label` slot' do
before do
fake_form_for do |form|
render_inline(
described_class.new(
form: form,
method: method
)
) do |c|
c.label { label }
end
end
end
include_examples 'it renders unchecked checkbox with value of `1`'
end
context 'with `help_text` slot' do
before do
fake_form_for do |form|
render_inline(
described_class.new(
form: form,
method: method,
label: label
)
) do |c|
c.help_text { help_text }
end
end
end
include_examples 'it renders unchecked checkbox with value of `1`'
include_examples 'it renders help text'
end
context 'with `label` and `help_text` slots' do
before do
fake_form_for do |form|
render_inline(
described_class.new(
form: form,
method: method
)
) do |c|
c.label { label }
c.help_text { help_text }
end
end
end
include_examples 'it renders unchecked checkbox with value of `1`'
include_examples 'it renders help text'
end
end

View File

@ -23,4 +23,21 @@ RSpec.describe Pajamas::Component do
expect(value).to eq('something')
end
end
describe '#format_options' do
it 'merges CSS classes and additional options' do
expect(
subject.send(
:format_options,
options: { foo: 'bar', class: 'gl-display-flex gl-py-5' },
css_classes: %w(gl-px-5 gl-mt-5),
additional_options: { baz: 'bax' }
)
).to match({
foo: 'bar',
baz: 'bax',
class: ['gl-px-5', 'gl-mt-5', 'gl-display-flex gl-py-5']
})
end
end
end

View File

@ -0,0 +1,110 @@
# frozen_string_literal: true
require "spec_helper"
RSpec.describe Pajamas::Concerns::CheckboxRadioLabelWithHelpText do
let(:form) { instance_double('ActionView::Helpers::FormBuilder') }
let(:component_class) do
Class.new do
attr_reader(
:form,
:method,
:label_argument,
:help_text_argument,
:label_options,
:input_options,
:value
)
def initialize(
form:,
method:,
label: nil,
help_text: nil,
label_options: {},
radio_options: {},
value: nil
)
@form = form
@method = method
@label_argument = label
@help_text_argument = help_text
@label_options = label_options
@input_options = radio_options
@value = value
end
def label_content
@label_argument
end
def help_text_content
@help_text_argument
end
def format_options(options:, css_classes: [], additional_options: {})
{}
end
include Pajamas::Concerns::CheckboxRadioLabelWithHelpText
include ActionView::Helpers::TagHelper
end
end
let_it_be(:method) { 'username' }
let_it_be(:label_options) { { class: 'foo-bar' } }
let_it_be(:value) { 'Foo bar' }
describe '#render_label_with_help_text' do
it 'calls `#format_options` with correct arguments' do
allow(form).to receive(:label)
component = component_class.new(form: form, method: method, label_options: label_options, value: value)
expect(component).to receive(:format_options).with(
options: label_options,
css_classes: ['custom-control-label'],
additional_options: { value: value }
)
component.render_label_with_help_text
end
context 'when `help_text` argument is passed' do
it 'calls `form.label` with `label` and `help_text` arguments used in the block' do
component = component_class.new(
form: form,
method: method,
label: 'Label argument',
help_text: 'Help text argument'
)
expected_label_entry = '<span>Label argument</span><p class="help-text"' \
' data-testid="pajamas-component-help-text">Help text argument</p>'
expect(form).to receive(:label).with(method, {}) do |&block|
expect(block.call).to eq(expected_label_entry)
end
component.render_label_with_help_text
end
end
context 'when `help_text` argument is not passed' do
it 'calls `form.label` with `label` argument used in the block' do
component = component_class.new(
form: form,
method: method,
label: 'Label argument'
)
expected_label_entry = '<span>Label argument</span>'
expect(form).to receive(:label).with(method, {}) do |&block|
expect(block.call).to eq(expected_label_entry)
end
component.render_label_with_help_text
end
end
end
end

View File

@ -0,0 +1,32 @@
# frozen_string_literal: true
require "spec_helper"
RSpec.describe Pajamas::Concerns::CheckboxRadioOptions do
let(:component_class) do
Class.new do
include Pajamas::Concerns::CheckboxRadioOptions
attr_reader(:input_options)
def initialize(input_options: {})
@input_options = input_options
end
def format_options(options:, css_classes: [], additional_options: {})
{}
end
end
end
describe '#formatted_input_options' do
let_it_be(:input_options) { { class: 'foo-bar' } }
it 'calls `#format_options` with correct arguments' do
component = component_class.new(input_options: input_options)
expect(component).to receive(:format_options).with(options: input_options, css_classes: ['custom-control-input'])
component.formatted_input_options
end
end
end

View File

@ -0,0 +1,126 @@
# frozen_string_literal: true
require "spec_helper"
RSpec.describe Pajamas::RadioComponent, :aggregate_failures, type: :component do
include FormBuilderHelpers
let_it_be(:method) { :access_level }
let_it_be(:label) { "Access Level" }
let_it_be(:value) { :regular }
let_it_be(:help_text) do
'Administrators have access to all groups, projects, and users and can manage all features in this installation'
end
RSpec.shared_examples 'it renders unchecked radio' do
it 'renders unchecked radio' do
expect(rendered_component).to have_unchecked_field(label)
end
end
context 'with default options' do
before do
fake_form_for do |form|
render_inline(
described_class.new(
form: form,
method: method,
value: value,
label: label
)
)
end
end
include_examples 'it renders unchecked radio'
include_examples 'it does not render help text'
end
context 'with custom options' do
let_it_be(:radio_options) { { class: 'radio-foo-bar', checked: true } }
let_it_be(:label_options) { { class: 'label-foo-bar' } }
before do
fake_form_for do |form|
render_inline(
described_class.new(
form: form,
method: method,
value: method,
label: label,
help_text: help_text,
radio_options: radio_options,
label_options: label_options
)
)
end
end
include_examples 'it renders help text'
it 'renders checked radio' do
expect(rendered_component).to have_checked_field(label, class: radio_options[:class])
end
it 'adds CSS class to label' do
expect(rendered_component).to have_selector('label.label-foo-bar')
end
end
context 'with `label` slot' do
before do
fake_form_for do |form|
render_inline(
described_class.new(
form: form,
method: method,
value: value
)
) do |c|
c.label { label }
end
end
end
include_examples 'it renders unchecked radio'
end
context 'with `help_text` slot' do
before do
fake_form_for do |form|
render_inline(
described_class.new(
form: form,
method: method,
value: value,
label: label
)
) do |c|
c.help_text { help_text }
end
end
end
include_examples 'it renders unchecked radio'
include_examples 'it renders help text'
end
context 'with `label` and `help_text` slots' do
before do
fake_form_for do |form|
render_inline(
described_class.new(
form: form,
method: method,
value: value
)
) do |c|
c.label { label }
c.help_text { help_text }
end
end
end
include_examples 'it renders unchecked radio'
include_examples 'it renders help text'
end
end

View File

@ -1,6 +1,6 @@
import { request } from 'axios';
export function getMetadata(endpoint) {
export function getDiffsMetadata(endpoint) {
const { url } = endpoint;
return request({
@ -22,7 +22,7 @@ export function getDiscussions(endpoint) {
}).then((response) => response.data);
}
export function getDiffs(endpoint) {
export function getDiffsBatch(endpoint) {
const { url } = endpoint;
return request({

View File

@ -62,7 +62,7 @@ const body = {
},
};
const Diffs = {
const DiffsBatch = {
body: Matchers.extractPayload(body),
success: {
@ -86,5 +86,6 @@ const Diffs = {
},
};
export { Diffs };
export { DiffsBatch };
/* eslint-enable @gitlab/require-i18n-strings */

View File

@ -70,7 +70,7 @@ const body = {
project_name: Matchers.string('contract-testing'),
};
const Metadata = {
const DiffsMetadata = {
body: Matchers.extractPayload(body),
success: {
@ -82,7 +82,7 @@ const Metadata = {
},
request: {
uponReceiving: 'a request for Metadata',
uponReceiving: 'a request for Diffs Metadata',
withRequest: {
method: 'GET',
path: '/gitlab-org/gitlab-qa/-/merge_requests/1/diffs_metadata.json',
@ -93,5 +93,6 @@ const Metadata = {
},
};
export { Metadata };
export { DiffsMetadata };
/* eslint-enable @gitlab/require-i18n-strings */

View File

@ -83,4 +83,5 @@ const Discussions = {
};
export { Discussions };
/* eslint-enable @gitlab/require-i18n-strings */

View File

@ -1,37 +0,0 @@
/* eslint-disable @gitlab/require-i18n-strings */
import { pactWith } from 'jest-pact';
import { Diffs } from '../fixtures/diffs.fixture';
import { getDiffs } from '../endpoints/merge_requests';
pactWith(
{
consumer: 'Merge Request Page',
provider: 'Merge Request Diffs Endpoint',
log: '../logs/consumer.log',
dir: '../contracts',
},
(provider) => {
describe('Diffs Endpoint', () => {
beforeEach(() => {
const interaction = {
state: 'a merge request with diffs exists',
...Diffs.request,
willRespondWith: Diffs.success,
};
provider.addInteraction(interaction);
});
it('return a successful body', () => {
return getDiffs({
url: provider.mockService.baseUrl,
}).then((diffs) => {
expect(diffs).toEqual(Diffs.body);
});
});
});
},
);
/* eslint-enable @gitlab/require-i18n-strings */

View File

@ -1,37 +0,0 @@
/* eslint-disable @gitlab/require-i18n-strings */
import { pactWith } from 'jest-pact';
import { Discussions } from '../fixtures/discussions.fixture';
import { getDiscussions } from '../endpoints/merge_requests';
pactWith(
{
consumer: 'Merge Request Page',
provider: 'Merge Request Discussions Endpoint',
log: '../logs/consumer.log',
dir: '../contracts',
},
(provider) => {
describe('Discussions Endpoint', () => {
beforeEach(() => {
const interaction = {
state: 'a merge request with discussions exists',
...Discussions.request,
willRespondWith: Discussions.success,
};
provider.addInteraction(interaction);
});
it('return a successful body', () => {
return getDiscussions({
url: provider.mockService.baseUrl,
}).then((discussions) => {
expect(discussions).toEqual(Discussions.body);
});
});
});
},
);
/* eslint-enable @gitlab/require-i18n-strings */

View File

@ -1,37 +0,0 @@
/* eslint-disable @gitlab/require-i18n-strings */
import { pactWith } from 'jest-pact';
import { Metadata } from '../fixtures/metadata.fixture';
import { getMetadata } from '../endpoints/merge_requests';
pactWith(
{
consumer: 'Merge Request Page',
provider: 'Merge Request Metadata Endpoint',
log: '../logs/consumer.log',
dir: '../contracts',
},
(provider) => {
describe('Metadata Endpoint', () => {
beforeEach(() => {
const interaction = {
state: 'a merge request exists',
...Metadata.request,
willRespondWith: Metadata.success,
};
provider.addInteraction(interaction);
});
it('return a successful body', () => {
return getMetadata({
url: provider.mockService.baseUrl,
}).then((metadata) => {
expect(metadata).toEqual(Metadata.body);
});
});
});
},
);
/* eslint-enable @gitlab/require-i18n-strings */

View File

@ -0,0 +1,112 @@
/* eslint-disable @gitlab/require-i18n-strings */
import { pactWith } from 'jest-pact';
import { DiffsBatch } from '../../../fixtures/project/merge_request/diffs_batch.fixture';
import { Discussions } from '../../../fixtures/project/merge_request/discussions.fixture';
import { DiffsMetadata } from '../../../fixtures/project/merge_request/diffs_metadata.fixture';
import {
getDiffsBatch,
getDiffsMetadata,
getDiscussions,
} from '../../../endpoints/project/merge_requests';
const CONSUMER_NAME = 'MergeRequest#show';
const CONSUMER_LOG = '../logs/consumer.log';
const CONTRACT_DIR = '../contracts/project/merge_request/show';
const DIFFS_BATCH_PROVIDER_NAME = 'Merge Request Diffs Batch Endpoint';
const DISCUSSIONS_PROVIDER_NAME = 'Merge Request Discussions Endpoint';
const DIFFS_METADATA_PROVIDER_NAME = 'Merge Request Diffs Metadata Endpoint';
// API endpoint: /merge_requests/:id/diffs_batch.json
pactWith(
{
consumer: CONSUMER_NAME,
provider: DIFFS_BATCH_PROVIDER_NAME,
log: CONSUMER_LOG,
dir: CONTRACT_DIR,
},
(provider) => {
describe(DIFFS_BATCH_PROVIDER_NAME, () => {
beforeEach(() => {
const interaction = {
state: 'a merge request with diffs exists',
...DiffsBatch.request,
willRespondWith: DiffsBatch.success,
};
provider.addInteraction(interaction);
});
it('returns a successful body', () => {
return getDiffsBatch({
url: provider.mockService.baseUrl,
}).then((diffsBatch) => {
expect(diffsBatch).toEqual(DiffsBatch.body);
});
});
});
},
);
pactWith(
{
consumer: CONSUMER_NAME,
provider: DISCUSSIONS_PROVIDER_NAME,
log: CONSUMER_LOG,
dir: CONTRACT_DIR,
},
(provider) => {
describe(DISCUSSIONS_PROVIDER_NAME, () => {
beforeEach(() => {
const interaction = {
state: 'a merge request with discussions exists',
...Discussions.request,
willRespondWith: Discussions.success,
};
provider.addInteraction(interaction);
});
it('return a successful body', () => {
return getDiscussions({
url: provider.mockService.baseUrl,
}).then((discussions) => {
expect(discussions).toEqual(Discussions.body);
});
});
});
},
);
pactWith(
{
consumer: CONSUMER_NAME,
provider: DIFFS_METADATA_PROVIDER_NAME,
log: CONSUMER_LOG,
dir: CONTRACT_DIR,
},
(provider) => {
describe(DIFFS_METADATA_PROVIDER_NAME, () => {
beforeEach(() => {
const interaction = {
state: 'a merge request exists',
...DiffsMetadata.request,
willRespondWith: DiffsMetadata.success,
};
provider.addInteraction(interaction);
});
it('return a successful body', () => {
return getDiffsMetadata({
url: provider.mockService.baseUrl,
}).then((diffsMetadata) => {
expect(diffsMetadata).toEqual(DiffsMetadata.body);
});
});
});
},
);
/* eslint-enable @gitlab/require-i18n-strings */

View File

@ -1,9 +1,9 @@
{
"consumer": {
"name": "Merge Request Page"
"name": "MergeRequest#show"
},
"provider": {
"name": "Merge Request Diffs Endpoint"
"name": "Merge Request Diffs Batch Endpoint"
},
"interactions": [
{
@ -226,4 +226,4 @@
"version": "2.0.0"
}
}
}
}

View File

@ -1,13 +1,13 @@
{
"consumer": {
"name": "Merge Request Page"
"name": "MergeRequest#show"
},
"provider": {
"name": "Merge Request Metadata Endpoint"
"name": "Merge Request Diffs Metadata Endpoint"
},
"interactions": [
{
"description": "a request for Metadata",
"description": "a request for Diffs Metadata",
"providerState": "a merge request exists",
"request": {
"method": "GET",
@ -220,4 +220,4 @@
"version": "2.0.0"
}
}
}
}

View File

@ -1,6 +1,6 @@
{
"consumer": {
"name": "Merge Request Page"
"name": "MergeRequest#show"
},
"provider": {
"name": "Merge Request Discussions Endpoint"
@ -233,4 +233,4 @@
"version": "2.0.0"
}
}
}
}

View File

@ -0,0 +1,16 @@
# frozen_string_literal: true
require_relative '../../../spec_helper'
require_relative '../../../states/project/merge_request/diffs_batch_state'
module Provider
module DiffsBatchHelper
Pact.service_provider "Merge Request Diffs Batch Endpoint" do
app { Environments::Test.app }
honours_pact_with 'MergeRequest#show' do
pact_uri '../contracts/project/merge_request/show/mergerequest#show-merge_request_diffs_batch_endpoint.json'
end
end
end
end

View File

@ -0,0 +1,16 @@
# frozen_string_literal: true
require_relative '../../../spec_helper'
require_relative '../../../states/project/merge_request/diffs_metadata_state'
module Provider
module DiffsMetadataHelper
Pact.service_provider "Merge Request Diffs Metadata Endpoint" do
app { Environments::Test.app }
honours_pact_with 'MergeRequest#show' do
pact_uri '../contracts/project/merge_request/show/mergerequest#show-merge_request_diffs_metadata_endpoint.json'
end
end
end
end

View File

@ -0,0 +1,16 @@
# frozen_string_literal: true
require_relative '../../../spec_helper'
require_relative '../../../states/project/merge_request/discussions_state'
module Provider
module DiscussionsHelper
Pact.service_provider "Merge Request Discussions Endpoint" do
app { Environments::Test.app }
honours_pact_with 'MergeRequest#show' do
pact_uri '../contracts/project/merge_request/show/mergerequest#show-merge_request_discussions_endpoint.json'
end
end
end
end

View File

@ -1,16 +0,0 @@
# frozen_string_literal: true
require_relative '../spec_helper'
require_relative '../states/diffs_state'
module Provider
module DiffsHelper
Pact.service_provider "Merge Request Diffs Endpoint" do
app { Environments::Test.app }
honours_pact_with 'Merge Request Page' do
pact_uri '../contracts/merge_request_page-merge_request_diffs_endpoint.json'
end
end
end
end

View File

@ -1,16 +0,0 @@
# frozen_string_literal: true
require_relative '../spec_helper'
require_relative '../states/discussions_state'
module Provider
module DiscussionsHelper
Pact.service_provider "Merge Request Discussions Endpoint" do
app { Environments::Test.app }
honours_pact_with 'Merge Request Page' do
pact_uri '../contracts/merge_request_page-merge_request_discussions_endpoint.json'
end
end
end
end

View File

@ -1,16 +0,0 @@
# frozen_string_literal: true
require_relative '../spec_helper'
require_relative '../states/metadata_state'
module Provider
module MetadataHelper
Pact.service_provider "Merge Request Metadata Endpoint" do
app { Environments::Test.app }
honours_pact_with 'Merge Request Page' do
pact_uri '../contracts/merge_request_page-merge_request_metadata_endpoint.json'
end
end
end
end

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
Pact.provider_states_for "Merge Request Page" do
Pact.provider_states_for "MergeRequest#show" do
provider_state "a merge request with diffs exists" do
set_up do
user = User.find_by(name: Provider::UsersHelper::CONTRACT_USER_NAME)

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
Pact.provider_states_for "Merge Request Page" do
Pact.provider_states_for "MergeRequest#show" do
provider_state "a merge request exists" do
set_up do
user = User.find_by(name: Provider::UsersHelper::CONTRACT_USER_NAME)

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
Pact.provider_states_for "Merge Request Page" do
Pact.provider_states_for "MergeRequest#show" do
provider_state "a merge request with discussions exists" do
set_up do
user = User.find_by(name: Provider::UsersHelper::CONTRACT_USER_NAME)

View File

@ -148,19 +148,19 @@ RSpec.describe Projects::ReleasesController do
end
let(:release) { create(:release, project: project) }
let(:tag) { CGI.escape(release.tag) }
let(:tag) { release.tag }
it_behaves_like 'successful request'
context 'when tag name contains slash' do
let(:release) { create(:release, project: project, tag: 'awesome/v1.0') }
let(:tag) { CGI.escape(release.tag) }
let(:tag) { release.tag }
it_behaves_like 'successful request'
it 'is accesible at a URL encoded path' do
expect(edit_project_release_path(project, release))
.to eq("/#{project.namespace.path}/#{project.name}/-/releases/awesome%252Fv1.0/edit")
.to eq("/#{project.namespace.path}/#{project.name}/-/releases/awesome%2Fv1.0/edit")
end
end
@ -187,19 +187,19 @@ RSpec.describe Projects::ReleasesController do
end
let(:release) { create(:release, project: project) }
let(:tag) { CGI.escape(release.tag) }
let(:tag) { release.tag }
it_behaves_like 'successful request'
context 'when tag name contains slash' do
let(:release) { create(:release, project: project, tag: 'awesome/v1.0') }
let(:tag) { CGI.escape(release.tag) }
let(:tag) { release.tag }
it_behaves_like 'successful request'
it 'is accesible at a URL encoded path' do
expect(project_release_path(project, release))
.to eq("/#{project.namespace.path}/#{project.name}/-/releases/awesome%252Fv1.0")
.to eq("/#{project.namespace.path}/#{project.name}/-/releases/awesome%2Fv1.0")
end
end
@ -239,7 +239,7 @@ RSpec.describe Projects::ReleasesController do
end
let(:release) { create(:release, project: project) }
let(:tag) { CGI.escape(release.tag) }
let(:tag) { release.tag }
context 'when user is a guest' do
let(:project) { private_project }

View File

@ -468,4 +468,25 @@ RSpec.describe DiffHelper do
it { is_expected.to be_nil }
end
end
describe '#conflicts' do
let(:merge_request) { instance_double(MergeRequest) }
before do
allow(helper).to receive(:merge_request).and_return(merge_request)
allow(helper).to receive(:options).and_return(merge_ref_head_diff: true)
end
context 'when Gitlab::Git::Conflict::Resolver::ConflictSideMissing exception is raised' do
before do
allow_next_instance_of(MergeRequests::Conflicts::ListService, merge_request, allow_tree_conflicts: true) do |svc|
allow(svc).to receive_message_chain(:conflicts, :files).and_raise(Gitlab::Git::Conflict::Resolver::ConflictSideMissing)
end
end
it 'returns an empty hash' do
expect(helper.conflicts(allow_tree_conflicts: true)).to eq({})
end
end
end
end

View File

@ -3,163 +3,194 @@
require 'spec_helper'
RSpec.describe Gitlab::FormBuilders::GitlabUiFormBuilder do
let_it_be(:user) { build(:user) }
let_it_be(:fake_template) do
Object.new.tap do |template|
template.extend ActionView::Helpers::FormHelper
template.extend ActionView::Helpers::FormOptionsHelper
template.extend ActionView::Helpers::TagHelper
template.extend ActionView::Context
end
end
include FormBuilderHelpers
let_it_be(:form_builder) { described_class.new(:user, user, fake_template, {}) }
let_it_be(:user) { build(:user, :admin) }
let_it_be(:form_builder) { described_class.new(:user, user, fake_action_view_base, {}) }
describe '#gitlab_ui_checkbox_component' do
let(:optional_args) { {} }
context 'when not using slots' do
let(:optional_args) { {} }
subject(:checkbox_html) { form_builder.gitlab_ui_checkbox_component(:view_diffs_file_by_file, "Show one file at a time on merge request's Changes tab", **optional_args) }
subject(:checkbox_html) do
form_builder.gitlab_ui_checkbox_component(
:view_diffs_file_by_file,
"Show one file at a time on merge request's Changes tab",
**optional_args
)
end
context 'without optional arguments' do
it 'renders correct html' do
expected_html = <<~EOS
<div class="gl-form-checkbox custom-control custom-checkbox">
<input name="user[view_diffs_file_by_file]" type="hidden" value="0" />
<input class="custom-control-input" type="checkbox" value="1" name="user[view_diffs_file_by_file]" id="user_view_diffs_file_by_file" />
<label class="custom-control-label" for="user_view_diffs_file_by_file">
<span>Show one file at a time on merge request&#39;s Changes tab</span>
</label>
</div>
EOS
expect(html_strip_whitespace(checkbox_html)).to eq(html_strip_whitespace(expected_html))
end
end
context 'with optional arguments' do
let(:optional_args) do
{
help_text: 'Instead of all the files changed, show only one file at a time.',
checkbox_options: { class: 'checkbox-foo-bar' },
label_options: { class: 'label-foo-bar' },
checked_value: '3',
unchecked_value: '1'
}
end
it 'renders help text' do
expected_html = <<~EOS
<div class="gl-form-checkbox custom-control custom-checkbox">
<input name="user[view_diffs_file_by_file]" type="hidden" value="1" />
<input class="custom-control-input checkbox-foo-bar" type="checkbox" value="3" name="user[view_diffs_file_by_file]" id="user_view_diffs_file_by_file" />
<label class="custom-control-label label-foo-bar" for="user_view_diffs_file_by_file">
<span>Show one file at a time on merge request&#39;s Changes tab</span>
<p class="help-text" data-testid="pajamas-component-help-text">Instead of all the files changed, show only one file at a time.</p>
</label>
</div>
EOS
expect(html_strip_whitespace(checkbox_html)).to eq(html_strip_whitespace(expected_html))
end
end
context 'with checkbox_options: { multiple: true }' do
let(:optional_args) do
{
checkbox_options: { multiple: true },
checked_value: 'one',
unchecked_value: false
}
end
it 'renders labels with correct for attributes' do
expected_html = <<~EOS
<div class="gl-form-checkbox custom-control custom-checkbox">
<input class="custom-control-input" type="checkbox" value="one" name="user[view_diffs_file_by_file][]" id="user_view_diffs_file_by_file_one" />
<label class="custom-control-label" for="user_view_diffs_file_by_file_one">
<span>Show one file at a time on merge request&#39;s Changes tab</span>
</label>
</div>
EOS
expect(html_strip_whitespace(checkbox_html)).to eq(html_strip_whitespace(expected_html))
end
end
end
context 'when using slots' do
subject(:checkbox_html) do
form_builder.gitlab_ui_checkbox_component(
:view_diffs_file_by_file
) do |c|
c.label { "Show one file at a time on merge request's Changes tab" }
c.help_text { 'Instead of all the files changed, show only one file at a time.' }
end
end
context 'without optional arguments' do
it 'renders correct html' do
expected_html = <<~EOS
<div class="gl-form-checkbox custom-control custom-checkbox">
<input name="user[view_diffs_file_by_file]" type="hidden" value="0" />
<input class="custom-control-input" type="checkbox" value="1" name="user[view_diffs_file_by_file]" id="user_view_diffs_file_by_file" />
<label class="custom-control-label" for="user_view_diffs_file_by_file">
Show one file at a time on merge request&#39;s Changes tab
</label>
</div>
EOS
expect(checkbox_html).to eq(html_strip_whitespace(expected_html))
end
end
context 'with optional arguments' do
let(:optional_args) do
{
help_text: 'Instead of all the files changed, show only one file at a time.',
checkbox_options: { class: 'checkbox-foo-bar' },
label_options: { class: 'label-foo-bar' },
checked_value: '3',
unchecked_value: '1'
}
end
it 'renders help text' do
expected_html = <<~EOS
<div class="gl-form-checkbox custom-control custom-checkbox">
<input name="user[view_diffs_file_by_file]" type="hidden" value="1" />
<input class="custom-control-input checkbox-foo-bar" type="checkbox" value="3" name="user[view_diffs_file_by_file]" id="user_view_diffs_file_by_file" />
<label class="custom-control-label label-foo-bar" for="user_view_diffs_file_by_file">
<span>Show one file at a time on merge request&#39;s Changes tab</span>
<p class="help-text">Instead of all the files changed, show only one file at a time.</p>
<p class="help-text" data-testid="pajamas-component-help-text">Instead of all the files changed, show only one file at a time.</p>
</label>
</div>
EOS
expect(checkbox_html).to eq(html_strip_whitespace(expected_html))
end
it 'passes arguments to `check_box` method' do
allow(fake_template).to receive(:check_box).and_return('')
checkbox_html
expect(fake_template).to have_received(:check_box).with(:user, :view_diffs_file_by_file, { class: %w(custom-control-input checkbox-foo-bar), object: user }, '3', '1')
end
it 'passes arguments to `label` method' do
allow(fake_template).to receive(:label).and_return('')
checkbox_html
expect(fake_template).to have_received(:label).with(:user, :view_diffs_file_by_file, { class: %w(custom-control-label label-foo-bar), object: user, value: nil })
end
end
context 'with checkbox_options: { multiple: true }' do
let(:optional_args) do
{
checkbox_options: { multiple: true },
checked_value: 'one',
unchecked_value: false
}
end
it 'renders labels with correct for attributes' do
expected_html = <<~EOS
<div class="gl-form-checkbox custom-control custom-checkbox">
<input class="custom-control-input" type="checkbox" value="one" name="user[view_diffs_file_by_file][]" id="user_view_diffs_file_by_file_one" />
<label class="custom-control-label" for="user_view_diffs_file_by_file_one">
Show one file at a time on merge request&#39;s Changes tab
</label>
</div>
EOS
expect(checkbox_html).to eq(html_strip_whitespace(expected_html))
expect(html_strip_whitespace(checkbox_html)).to eq(html_strip_whitespace(expected_html))
end
end
end
describe '#gitlab_ui_radio_component' do
let(:optional_args) { {} }
context 'when not using slots' do
let(:optional_args) { {} }
subject(:radio_html) { form_builder.gitlab_ui_radio_component(:access_level, :admin, "Access Level", **optional_args) }
subject(:radio_html) do
form_builder.gitlab_ui_radio_component(
:access_level,
:admin,
"Admin",
**optional_args
)
end
context 'without optional arguments' do
it 'renders correct html' do
expected_html = <<~EOS
<div class="gl-form-radio custom-control custom-radio">
<input class="custom-control-input" type="radio" value="admin" name="user[access_level]" id="user_access_level_admin" />
<label class="custom-control-label" for="user_access_level_admin">
Access Level
</label>
</div>
EOS
context 'without optional arguments' do
it 'renders correct html' do
expected_html = <<~EOS
<div class="gl-form-radio custom-control custom-radio">
<input class="custom-control-input" type="radio" value="admin" checked="checked" name="user[access_level]" id="user_access_level_admin" />
<label class="custom-control-label" for="user_access_level_admin">
<span>Admin</span>
</label>
</div>
EOS
expect(radio_html).to eq(html_strip_whitespace(expected_html))
expect(html_strip_whitespace(radio_html)).to eq(html_strip_whitespace(expected_html))
end
end
context 'with optional arguments' do
let(:optional_args) do
{
help_text: 'Administrators have access to all groups, projects, and users and can manage all features in this installation',
radio_options: { class: 'radio-foo-bar' },
label_options: { class: 'label-foo-bar' }
}
end
it 'renders help text' do
expected_html = <<~EOS
<div class="gl-form-radio custom-control custom-radio">
<input class="custom-control-input radio-foo-bar" type="radio" value="admin" checked="checked" name="user[access_level]" id="user_access_level_admin" />
<label class="custom-control-label label-foo-bar" for="user_access_level_admin">
<span>Admin</span>
<p class="help-text" data-testid="pajamas-component-help-text">Administrators have access to all groups, projects, and users and can manage all features in this installation</p>
</label>
</div>
EOS
expect(html_strip_whitespace(radio_html)).to eq(html_strip_whitespace(expected_html))
end
end
end
context 'with optional arguments' do
let(:optional_args) do
{
help_text: 'Administrators have access to all groups, projects, and users and can manage all features in this installation',
radio_options: { class: 'radio-foo-bar' },
label_options: { class: 'label-foo-bar' }
}
context 'when using slots' do
subject(:radio_html) do
form_builder.gitlab_ui_radio_component(
:access_level,
:admin
) do |c|
c.label { "Admin" }
c.help_text { 'Administrators have access to all groups, projects, and users and can manage all features in this installation' }
end
end
it 'renders help text' do
it 'renders correct html' do
expected_html = <<~EOS
<div class="gl-form-radio custom-control custom-radio">
<input class="custom-control-input radio-foo-bar" type="radio" value="admin" name="user[access_level]" id="user_access_level_admin" />
<label class="custom-control-label label-foo-bar" for="user_access_level_admin">
<span>Access Level</span>
<p class="help-text">Administrators have access to all groups, projects, and users and can manage all features in this installation</p>
<input class="custom-control-input" type="radio" value="admin" checked="checked" name="user[access_level]" id="user_access_level_admin" />
<label class="custom-control-label" for="user_access_level_admin">
<span>Admin</span>
<p class="help-text" data-testid="pajamas-component-help-text">Administrators have access to all groups, projects, and users and can manage all features in this installation</p>
</label>
</div>
EOS
expect(radio_html).to eq(html_strip_whitespace(expected_html))
end
it 'passes arguments to `radio_button` method' do
allow(fake_template).to receive(:radio_button).and_return('')
radio_html
expect(fake_template).to have_received(:radio_button).with(:user, :access_level, :admin, { class: %w(custom-control-input radio-foo-bar), object: user })
end
it 'passes arguments to `label` method' do
allow(fake_template).to receive(:label).and_return('')
radio_html
expect(fake_template).to have_received(:label).with(:user, :access_level, { class: %w(custom-control-label label-foo-bar), object: user, value: :admin })
expect(html_strip_whitespace(radio_html)).to eq(html_strip_whitespace(expected_html))
end
end
end

View File

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe API::Internal::Workhorse do
RSpec.describe API::Internal::Workhorse, :allow_forgery_protection do
include WorkhorseHelpers
context '/authorize_upload' do

View File

@ -227,6 +227,7 @@ RSpec.describe API::Releases do
get api("/projects/#{project.id}/releases", maintainer)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response[0]['tag_path']).to include('%2F') # properly escape the slash
end
end

View File

@ -7,12 +7,6 @@ RSpec.describe MergeRequests::Mergeability::RunChecksService do
let_it_be(:merge_request) { create(:merge_request) }
describe '#CHECKS' do
it 'contains every subclass of the base checks service', :eager_load do
expect(described_class::CHECKS).to contain_exactly(*MergeRequests::Mergeability::CheckBaseService.subclasses)
end
end
describe '#execute' do
subject(:execute) { run_checks.execute }
@ -22,8 +16,8 @@ RSpec.describe MergeRequests::Mergeability::RunChecksService do
context 'when every check is skipped', :eager_load do
before do
MergeRequests::Mergeability::CheckBaseService.subclasses.each do |subclass|
expect_next_instance_of(subclass) do |service|
expect(service).to receive(:skip?).and_return(true)
allow_next_instance_of(subclass) do |service|
allow(service).to receive(:skip?).and_return(true)
end
end
end
@ -35,7 +29,7 @@ RSpec.describe MergeRequests::Mergeability::RunChecksService do
context 'when a check is skipped' do
it 'does not execute the check' do
described_class::CHECKS.each do |check|
merge_request.mergeability_checks.each do |check|
allow_next_instance_of(check) do |service|
allow(service).to receive(:skip?).and_return(false)
allow(service).to receive(:execute).and_return(success_result)
@ -47,7 +41,13 @@ RSpec.describe MergeRequests::Mergeability::RunChecksService do
expect(service).not_to receive(:execute)
end
expect(execute).to match_array([success_result, success_result, success_result, success_result])
# Since we're only marking one check to be skipped, we expect to receive
# `# of checks - 1` success result objects in return
#
check_count = merge_request.mergeability_checks.count - 1
success_array = (1..check_count).each_with_object([]) { |_, array| array << success_result }
expect(execute).to match_array(success_array)
end
end
@ -56,7 +56,7 @@ RSpec.describe MergeRequests::Mergeability::RunChecksService do
let(:merge_check) { instance_double(MergeRequests::Mergeability::CheckCiStatusService) }
before do
described_class::CHECKS.each do |check|
merge_request.mergeability_checks.each do |check|
allow_next_instance_of(check) do |service|
allow(service).to receive(:skip?).and_return(true)
end

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
module FormBuilderHelpers
def fake_action_view_base
lookup_context = ActionView::LookupContext.new(ActionController::Base.view_paths)
ActionView::Base.new(lookup_context, {}, ApplicationController.new)
end
def fake_form_for(&block)
fake_action_view_base.form_for :user, url: '/user', &block
end
end

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
RSpec.shared_examples 'it renders help text' do
it 'renders help text' do
expect(rendered_component).to have_selector('[data-testid="pajamas-component-help-text"]', text: help_text)
end
end
RSpec.shared_examples 'it does not render help text' do
it 'does not render help text' do
expect(rendered_component).not_to have_selector('[data-testid="pajamas-component-help-text"]')
end
end

View File

@ -53,18 +53,20 @@ RSpec.shared_examples 'Gitlab-style deprecations' do
it 'adds information about the replacement if provided' do
deprecable = subject(deprecated: { milestone: '1.10', reason: :renamed, replacement: 'Foo.bar' })
expect(deprecable.deprecation_reason).to include 'Please use `Foo.bar`'
expect(deprecable.deprecation_reason).to include('Please use `Foo.bar`')
end
it 'supports named reasons: renamed' do
deprecable = subject(deprecated: { milestone: '1.10', reason: :renamed })
expect(deprecable.deprecation_reason).to include 'This was renamed.'
expect(deprecable.deprecation_reason).to eq('This was renamed. Deprecated in 1.10.')
end
it 'supports named reasons: alpha' do
deprecable = subject(deprecated: { milestone: '1.10', reason: :alpha })
expect(deprecable.deprecation_reason).to include 'This feature is in Alpha'
expect(deprecable.deprecation_reason).to eq(
'This feature is in Alpha. It can be changed or removed at any time. Introduced in 1.10.'
)
end
end

View File

@ -3,7 +3,6 @@ package api
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
@ -310,18 +309,18 @@ func (api *API) PreAuthorizeFixedPath(r *http.Request, method string, path strin
}
authReq.Header = helper.HeaderClone(r.Header)
ignoredResponse, apiResponse, err := api.PreAuthorize(path, authReq)
failureResponse, apiResponse, err := api.PreAuthorize(path, authReq)
if err != nil {
return nil, fmt.Errorf("PreAuthorize: %w", err)
}
// We don't need the contents of ignoredResponse but we are responsible
// We don't need the contents of failureResponse but we are responsible
// for closing it. Part of the reason PreAuthorizeFixedPath exists is to
// hide this awkwardness.
ignoredResponse.Body.Close()
failureResponse.Body.Close()
if apiResponse == nil {
return nil, errors.New("no api response on fixed path")
return nil, fmt.Errorf("no api response: status %d", failureResponse.StatusCode)
}
return apiResponse, nil