Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-02-17 21:14:40 +00:00
parent ae845b6277
commit 0c8b3354d9
28 changed files with 930 additions and 115 deletions

View File

@ -7,10 +7,11 @@ export const parsedData = (state) => {
state.chartData.forEach(({ date, author_name, author_email }) => {
total[date] = total[date] ? total[date] + 1 : 1;
const authorData = byAuthorEmail[author_email];
const normalizedEmail = author_email.toLowerCase();
const authorData = byAuthorEmail[normalizedEmail];
if (!authorData) {
byAuthorEmail[author_email] = {
byAuthorEmail[normalizedEmail] = {
name: author_name,
commits: 1,
dates: {

View File

@ -0,0 +1,64 @@
# frozen_string_literal: true
module Mutations
module WorkItems
class CreateFromTask < BaseMutation
include Mutations::SpamProtection
description "Creates a work item from a task in another work item's description." \
" Available only when feature flag `work_items` is enabled. This feature is experimental and is subject to change without notice."
graphql_name 'WorkItemCreateFromTask'
authorize :update_work_item
argument :id, ::Types::GlobalIDType[::WorkItem],
required: true,
description: 'Global ID of the work item.'
argument :work_item_data, ::Types::WorkItems::ConvertTaskInputType,
required: true,
description: 'Arguments necessary to convert a task into a work item.',
prepare: ->(attributes, _ctx) { attributes.to_h }
field :work_item, Types::WorkItemType,
null: true,
description: 'Updated work item.'
field :new_work_item, Types::WorkItemType,
null: true,
description: 'New work item created from task.'
def resolve(id:, work_item_data:)
work_item = authorized_find!(id: id)
unless Feature.enabled?(:work_items, work_item.project)
return { errors: ['`work_items` feature flag disabled for this project'] }
end
spam_params = ::Spam::SpamParams.new_from_request(request: context[:request])
result = ::WorkItems::CreateFromTaskService.new(
work_item: work_item,
current_user: current_user,
work_item_params: work_item_data,
spam_params: spam_params
).execute
check_spam_action_response!(result[:work_item]) if result[:work_item]
response = { errors: result.errors }
response.merge!(work_item: work_item, new_work_item: result[:work_item]) if result.success?
response
end
private
def find_object(id:)
# TODO: Remove coercion when working on https://gitlab.com/gitlab-org/gitlab/-/issues/257883
id = ::Types::GlobalIDType[::WorkItem].coerce_isolated_input(id)
GitlabSchema.find_by_gid(id)
end
end
end
end

View File

@ -1,5 +1,6 @@
# frozen_string_literal: true
# rubocop:disable Graphql/GraphqlNamePosition
module Types
class BaseEnum < GraphQL::Schema::Enum
class CustomValue < GraphQL::Schema::EnumValue

View File

@ -126,6 +126,7 @@ module Types
mount_mutation Mutations::Packages::DestroyFile
mount_mutation Mutations::Echo
mount_mutation Mutations::WorkItems::Create
mount_mutation Mutations::WorkItems::CreateFromTask
mount_mutation Mutations::WorkItems::Delete
mount_mutation Mutations::WorkItems::Update
end

View File

@ -0,0 +1,36 @@
# frozen_string_literal: true
module Types
module WorkItems
class ConvertTaskInputType < BaseInputObject
graphql_name 'WorkItemConvertTaskInput'
argument :line_number_end, GraphQL::Types::Int,
required: true,
description: 'Last line in the Markdown source that defines the list item task.'
argument :line_number_start, GraphQL::Types::Int,
required: true,
description: 'First line in the Markdown source that defines the list item task.'
argument :lock_version, GraphQL::Types::Int,
required: true,
description: 'Current lock version of the work item containing the task in the description.'
argument :title, GraphQL::Types::String,
required: true,
description: 'Full string of the task to be replaced. New title for the created work item.'
argument :work_item_type_id, ::Types::GlobalIDType[::WorkItems::Type],
required: true,
description: 'Global ID of the work item type used to create the new work item.',
prepare: ->(attribute, _ctx) { work_item_type_global_id(attribute) }
class << self
def work_item_type_global_id(global_id)
# TODO: remove this line when the compatibility layer is removed
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
global_id = ::Types::GlobalIDType[::WorkItems::Type].coerce_isolated_input(global_id)
global_id&.model_id
end
end
end
end
end

View File

@ -0,0 +1,43 @@
# frozen_string_literal: true
module WorkItems
# Create and link operations are not run inside a transaction in this class
# because CreateFromTaskService also creates a transaction.
# This class should always be run inside a transaction as we could end up with
# new work items that were never associated with other work items as expected.
class CreateAndLinkService
def initialize(project:, current_user: nil, params: {}, spam_params:, link_params: {})
@create_service = CreateService.new(
project: project,
current_user: current_user,
params: params,
spam_params: spam_params
)
@project = project
@current_user = current_user
@link_params = link_params
end
def execute
create_result = @create_service.execute
return create_result if create_result.error?
work_item = create_result[:work_item]
return ::ServiceResponse.success(payload: payload(work_item)) if @link_params.blank?
result = IssueLinks::CreateService.new(work_item, @current_user, @link_params).execute
if result[:status] == :success
::ServiceResponse.success(payload: payload(work_item))
else
::ServiceResponse.error(message: result[:message], http_status: 404)
end
end
private
def payload(work_item)
{ work_item: work_item }
end
end
end

View File

@ -0,0 +1,50 @@
# frozen_string_literal: true
module WorkItems
class CreateFromTaskService
def initialize(work_item:, current_user: nil, work_item_params: {}, spam_params:)
@work_item = work_item
@current_user = current_user
@work_item_params = work_item_params
@spam_params = spam_params
@errors = []
end
def execute
transaction_result = ApplicationRecord.transaction do
create_and_link_result = CreateAndLinkService.new(
project: @work_item.project,
current_user: @current_user,
params: @work_item_params.slice(:title, :work_item_type_id),
spam_params: @spam_params,
link_params: { target_issuable: @work_item }
).execute
if create_and_link_result.error?
@errors += create_and_link_result.errors
raise ActiveRecord::Rollback
end
replacement_result = TaskListReferenceReplacementService.new(
work_item: @work_item,
work_item_reference: create_and_link_result[:work_item].to_reference,
line_number_start: @work_item_params[:line_number_start],
line_number_end: @work_item_params[:line_number_end],
title: @work_item_params[:title],
lock_version: @work_item_params[:lock_version]
).execute
if replacement_result.error?
@errors += replacement_result.errors
raise ActiveRecord::Rollback
end
create_and_link_result
end
return transaction_result if transaction_result
::ServiceResponse.error(message: @errors, http_status: 422)
end
end
end

View File

@ -0,0 +1,52 @@
# frozen_string_literal: true
module WorkItems
class TaskListReferenceReplacementService
STALE_OBJECT_MESSAGE = 'Stale work item. Check lock version'
def initialize(work_item:, work_item_reference:, line_number_start:, line_number_end:, title:, lock_version:)
@work_item = work_item
@work_item_reference = work_item_reference
@line_number_start = line_number_start
@line_number_end = line_number_end
@title = title
@lock_version = lock_version
end
def execute
return ::ServiceResponse.error(message: STALE_OBJECT_MESSAGE) if @work_item.lock_version > @lock_version
return ::ServiceResponse.error(message: 'line_number_start must be greater than 0') if @line_number_start < 1
return ::ServiceResponse.error(message: 'line_number_end must be greater or equal to line_number_start') if @line_number_end < @line_number_start
return ::ServiceResponse.error(message: "Work item description can't be blank") if @work_item.description.blank?
source_lines = @work_item.description.split("\n")
markdown_task_first_line = source_lines[@line_number_start - 1]
task_line = Taskable::ITEM_PATTERN.match(markdown_task_first_line)
return ::ServiceResponse.error(message: "Unable to detect a task on line #{@line_number_start}") unless task_line
captures = task_line.captures
markdown_task_first_line.sub!(Taskable::ITEM_PATTERN, "#{captures[0]} #{captures[1]} #{@work_item_reference}+")
source_lines[@line_number_start - 1] = markdown_task_first_line
remove_additional_lines!(source_lines)
@work_item.update!(description: source_lines.join("\n"))
::ServiceResponse.success
rescue ActiveRecord::StaleObjectError
::ServiceResponse.error(message: STALE_OBJECT_MESSAGE)
end
private
def remove_additional_lines!(source_lines)
return if @line_number_end <= @line_number_start
source_lines.delete_if.each_with_index do |_line, index|
index >= @line_number_start && index < @line_number_end
end
end
end
end

View File

@ -0,0 +1,12 @@
- name: "GitLab self-monitoring" # The name of the feature to be deprecated
announcement_milestone: "14.8" # The milestone when this feature was first announced as deprecated.
announcement_date: "2022-02-22" # The date of the milestone release when this feature was first announced as deprecated. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post.
breaking_change: false # If this deprecation is a breaking change, set this value to true
reporter: abellucci # GitLab username of the person reporting the deprecation
body: | # Do not modify this line, instead modify the lines below.
GitLab self-monitoring gives administrators of self-hosted GitLab instances the tools to monitor the health of their instances. This feature is deprecated in GitLab 14.8, but is not scheduled for removal. For more information, see our official [Statement of Support](https://about.gitlab.com/support/statement-of-support.html#gitlab-self-monitoring).
# The following items are not published on the docs page, but may be used in the future.
stage: Monitor # (optional - may be required in the future) String value of the stage that the feature was created in. e.g., Growth
tiers: [Core, Premium, Ultimate] # (optional - may be required in the future) An array of tiers that the feature is available in currently. e.g., [Free, Silver, Gold, Core, Premium, Ultimate]
issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/348909 # (optional) This is a link to the deprecation issue in GitLab
documentation_url: https://docs.gitlab.com/ee/administration/monitoring/gitlab_self_monitoring_project/ # (optional) This is a link to the current documentation page

View File

@ -1704,3 +1704,38 @@ What does this mean? This strongly suggests that the S3 user does not have the r
[permissions to perform a HEAD request](https://docs.aws.amazon.com/AmazonS3/latest/API/API_HeadObject.html).
The solution: check the [IAM permissions again](https://docs.docker.com/registry/storage-drivers/s3/).
Once the right permissions were set, the error goes away.
### Missing `gitlab-registry.key` prevents container repository deletion
If you disable your GitLab instance's Container Registry and try to remove a project that has
container repositories, the following error occurs:
```plaintext
Errno::ENOENT: No such file or directory @ rb_sysopen - /var/opt/gitlab/gitlab-rails/etc/gitlab-registry.key
```
In this case, follow these steps:
1. Temporarily enable the instance-wide setting for the Container Registry in your `gitlab.rb`:
```ruby
gitlab_rails['registry_enabled'] = true
```
1. Save the file and [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure)
for the changes to take effect.
1. Try the removal again.
If you still can't remove the repository using the common methods, you can use the
[GitLab Rails console](../troubleshooting/navigating_gitlab_via_rails_console.md)
to remove the project by force:
```ruby
# Path to the project you'd like to remove
prj = Project.find_by_full_path(<project_path>)
# The following will delete the project's container registry, so be sure to double-check the path beforehand!
if prj.has_container_registry_tags?
prj.container_repositories.each { |p| p.destroy }
end
```

View File

@ -5178,6 +5178,29 @@ Input type: `WorkItemCreateInput`
| <a id="mutationworkitemcreateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationworkitemcreateworkitem"></a>`workItem` | [`WorkItem`](#workitem) | Created work item. |
### `Mutation.workItemCreateFromTask`
Creates a work item from a task in another work item's description. Available only when feature flag `work_items` is enabled. This feature is experimental and is subject to change without notice.
Input type: `WorkItemCreateFromTaskInput`
#### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationworkitemcreatefromtaskclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationworkitemcreatefromtaskid"></a>`id` | [`WorkItemID!`](#workitemid) | Global ID of the work item. |
| <a id="mutationworkitemcreatefromtaskworkitemdata"></a>`workItemData` | [`WorkItemConvertTaskInput!`](#workitemconverttaskinput) | Arguments necessary to convert a task into a work item. |
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationworkitemcreatefromtaskclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationworkitemcreatefromtaskerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationworkitemcreatefromtasknewworkitem"></a>`newWorkItem` | [`WorkItem`](#workitem) | New work item created from task. |
| <a id="mutationworkitemcreatefromtaskworkitem"></a>`workItem` | [`WorkItem`](#workitem) | Updated work item. |
### `Mutation.workItemDelete`
Deletes a work item. Available only when feature flag `work_items` is enabled. The feature is experimental and is subject to change without notice.
@ -19901,3 +19924,15 @@ A time-frame defined as a closed inclusive range of two dates.
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="vulnerabilityscannervendorinputname"></a>`name` | [`String!`](#string) | Name of the vendor/maintainer. |
### `WorkItemConvertTaskInput`
#### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="workitemconverttaskinputlinenumberend"></a>`lineNumberEnd` | [`Int!`](#int) | Last line in the Markdown source that defines the list item task. |
| <a id="workitemconverttaskinputlinenumberstart"></a>`lineNumberStart` | [`Int!`](#int) | First line in the Markdown source that defines the list item task. |
| <a id="workitemconverttaskinputlockversion"></a>`lockVersion` | [`Int!`](#int) | Current lock version of the work item containing the task in the description. |
| <a id="workitemconverttaskinputtitle"></a>`title` | [`String!`](#string) | Full string of the task to be replaced. New title for the created work item. |
| <a id="workitemconverttaskinputworkitemtypeid"></a>`workItemTypeId` | [`WorkItemsTypeID!`](#workitemstypeid) | Global ID of the work item type used to create the new work item. |

View File

@ -442,10 +442,10 @@ booleans:
```ruby
class MergeRequestPermissionsType < BasePermissionType
present_using MergeRequestPresenter
graphql_name 'MergeRequestPermissions'
present_using MergeRequestPresenter
abilities :admin_merge_request, :update_merge_request, :create_note
ability_field :resolve_note,
@ -1329,6 +1329,10 @@ class UserUpdateMutation < BaseMutation
end
```
Due to changes in the `1.13` version of the `graphql-ruby` gem, `graphql_name` should be the first
line of the class to ensure that type names are generated correctly. The `Graphql::GraphqlNamePosition` cop enforces this.
See [issue #27536](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/27536#note_840245581) for further context.
Our GraphQL mutation names are historically inconsistent, but new mutation names should follow the
convention `'{Resource}{Action}'` or `'{Resource}{Action}{Attribute}'`.
@ -1511,9 +1515,9 @@ GraphQL-name of the mutation:
```ruby
module Types
class MutationType < BaseObject
include Gitlab::Graphql::MountMutation
graphql_name 'Mutation'
graphql_name "Mutation"
include Gitlab::Graphql::MountMutation
mount_mutation Mutations::MergeRequests::SetDraft
end

View File

@ -41,8 +41,10 @@ least Maintainer [permissions](../user/permissions.md) to enable the Sentry inte
1. Sign up to Sentry.io or [deploy your own](#deploying-sentry) Sentry instance.
1. [Create](https://docs.sentry.io/product/sentry-basics/guides/integrate-frontend/create-new-project/) a new Sentry project. For each GitLab project that you want to integrate, we recommend that you create a new Sentry project.
1. [Find or generate](https://docs.sentry.io/api/auth/) a Sentry auth token for your Sentry project.
Make sure to give the token at least the following scopes: `event:read`, `project:read`, and `event:write` (for resolving events).
1. Find or generate a [Sentry auth token](https://docs.sentry.io/api/auth/#auth-tokens).
For the SaaS version of Sentry, you can find or generate the auth token at [https://sentry.io/api/](https://sentry.io/api/).
Make sure to give the token at least the following scopes: `project:read`, `event:read`, and
`event:write` (for resolving events).
1. In GitLab, enable error tracking:
1. On the top bar, select **Menu > Projects** and find your project.
1. On the left sidebar, select **Monitor > Error Tracking**.

View File

@ -900,6 +900,10 @@ To align with this change, API calls to list external status checks will also re
**Planned removal milestone: 15.0 (2022-05-22)**
### GitLab self-monitoring
GitLab self-monitoring gives administrators of self-hosted GitLab instances the tools to monitor the health of their instances. This feature is deprecated in GitLab 14.8, but is not scheduled for removal. For more information, see our official [Statement of Support](https://about.gitlab.com/support/statement-of-support.html#gitlab-self-monitoring).
### GraphQL ID and GlobalID compatibility
WARNING:

View File

@ -1,26 +0,0 @@
# frozen_string_literal: true
# This module has the necessary methods to render
# work items hierarchy menu
module Sidebars
module Concerns
module WorkItemHierarchy
def hierarchy_menu_item(container, url, path)
unless show_hierarachy_menu_item?(container)
return ::Sidebars::NilMenuItem.new(item_id: :hierarchy)
end
::Sidebars::MenuItem.new(
title: _('Planning hierarchy'),
link: url,
active_routes: { path: path },
item_id: :hierarchy
)
end
def show_hierarachy_menu_item?(container)
can?(context.current_user, :read_planning_hierarchy, container)
end
end
end
end

View File

@ -4,13 +4,10 @@ module Sidebars
module Projects
module Menus
class ProjectInformationMenu < ::Sidebars::Menu
include ::Sidebars::Concerns::WorkItemHierarchy
override :configure_menu_items
def configure_menu_items
add_item(activity_menu_item)
add_item(labels_menu_item)
add_item(hierarchy_menu_item(context.project, project_planning_hierarchy_path(context.project), 'projects#planning_hierarchy'))
add_item(members_menu_item)
true

View File

@ -49,10 +49,63 @@ module QA
end
context "when tls is disabled" do
where(:authentication_token_type, :token_name) do
:personal_access_token | 'Personal Access Token'
:project_deploy_token | 'Deploy Token'
:ci_job_token | 'Job Token'
where do
{
'using docker:18.09.9 and a personal access token' => {
docker_client_version: 'docker:18.09.9',
authentication_token_type: :personal_access_token,
token_name: 'Personal Access Token',
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/348499'
},
'using docker:18.09.9 and a project deploy token' => {
docker_client_version: 'docker:18.09.9',
authentication_token_type: :project_deploy_token,
token_name: 'Deploy Token',
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/348852'
},
'using docker:18.09.9 and a ci job token' => {
docker_client_version: 'docker:18.09.9',
authentication_token_type: :ci_job_token,
token_name: 'Job Token',
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/348765'
},
'using docker:19.03.12 and a personal access token' => {
docker_client_version: 'docker:19.03.12',
authentication_token_type: :personal_access_token,
token_name: 'Personal Access Token',
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/348507'
},
'using docker:19.03.12 and a project deploy token' => {
docker_client_version: 'docker:19.03.12',
authentication_token_type: :project_deploy_token,
token_name: 'Deploy Token',
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/348859'
},
'using docker:19.03.12 and a ci job token' => {
docker_client_version: 'docker:19.03.12',
authentication_token_type: :ci_job_token,
token_name: 'Job Token',
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/348654'
},
'using docker:20.10 and a personal access token' => {
docker_client_version: 'docker:20.10',
authentication_token_type: :personal_access_token,
token_name: 'Personal Access Token',
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/348754'
},
'using docker:20.10 and a project deploy token' => {
docker_client_version: 'docker:20.10',
authentication_token_type: :project_deploy_token,
token_name: 'Deploy Token',
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/348856'
},
'using docker:20.10 and a ci job token' => {
docker_client_version: 'docker:20.10',
authentication_token_type: :ci_job_token,
token_name: 'Job Token',
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/348766'
}
}
end
with_them do
@ -78,57 +131,51 @@ module QA
end
end
where(:docker_client_version) do
%w[docker:18.09.9 docker:19.03.12 docker:20.10]
end
with_them do
it "pushes image and deletes tag", :registry do
Support::Retrier.retry_on_exception(max_attempts: 3, sleep_interval: 2) do
Resource::Repository::Commit.fabricate_via_api! do |commit|
commit.project = project
commit.commit_message = 'Add .gitlab-ci.yml'
commit.add_files([{
file_path: '.gitlab-ci.yml',
content:
<<~YAML
build:
image: "#{docker_client_version}"
stage: build
services:
- name: "#{docker_client_version}-dind"
command: ["--insecure-registry=gitlab.test:5050"]
variables:
IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG
script:
- docker login -u #{auth_user} -p #{auth_token} gitlab.test:5050
- docker build -t $IMAGE_TAG .
- docker push $IMAGE_TAG
tags:
- "runner-for-#{project.name}"
YAML
}])
end
it "pushes image and deletes tag", :registry, testcase: params[:testcase] do
Support::Retrier.retry_on_exception(max_attempts: 3, sleep_interval: 2) do
Resource::Repository::Commit.fabricate_via_api! do |commit|
commit.project = project
commit.commit_message = 'Add .gitlab-ci.yml'
commit.add_files([{
file_path: '.gitlab-ci.yml',
content:
<<~YAML
build:
image: "#{docker_client_version}"
stage: build
services:
- name: "#{docker_client_version}-dind"
command: ["--insecure-registry=gitlab.test:5050"]
variables:
IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG
script:
- docker login -u #{auth_user} -p #{auth_token} gitlab.test:5050
- docker build -t $IMAGE_TAG .
- docker push $IMAGE_TAG
tags:
- "runner-for-#{project.name}"
YAML
}])
end
end
Flow::Pipeline.visit_latest_pipeline
Flow::Pipeline.visit_latest_pipeline
Page::Project::Pipeline::Show.perform do |pipeline|
pipeline.click_job('build')
end
Page::Project::Pipeline::Show.perform do |pipeline|
pipeline.click_job('build')
end
Page::Project::Job::Show.perform do |job|
expect(job).to be_successful(timeout: 800)
end
Page::Project::Job::Show.perform do |job|
expect(job).to be_successful(timeout: 800)
end
Page::Project::Menu.perform(&:go_to_container_registry)
Page::Project::Menu.perform(&:go_to_container_registry)
Page::Project::Registry::Show.perform do |registry|
expect(registry).to have_registry_repository(project.path_with_namespace)
Page::Project::Registry::Show.perform do |registry|
expect(registry).to have_registry_repository(project.path_with_namespace)
registry.click_on_image(project.path_with_namespace)
expect(registry).to have_tag('master')
end
registry.click_on_image(project.path_with_namespace)
expect(registry).to have_tag('master')
end
end
end
@ -156,7 +203,7 @@ module QA
apk add --no-cache openssl
true | openssl s_client -showcerts -connect gitlab.test:5050 > /usr/local/share/ca-certificates/gitlab.test.crt
update-ca-certificates
dockerd-entrypoint.sh || exit
dockerd-entrypoint.sh || exit
variables:
IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG
script:

View File

@ -0,0 +1,40 @@
# frozen_string_literal: true
# This cop ensures that if a class uses `graphql_name`, then
# it's the first line of the class
#
# @example
#
# # bad
# class AwfulClass
# field :some_field, GraphQL::Types::JSON
# graphql_name 'AwfulClass'
# end
#
# # good
# class GreatClass
# graphql_name 'AwfulClass'
# field :some_field, GraphQL::Types::String
# end
module RuboCop
module Cop
module Graphql
class GraphqlNamePosition < RuboCop::Cop::Cop
MSG = '`graphql_name` should be the first line of the class: '\
'https://docs.gitlab.com/ee/development/api_graphql_styleguide.html#naming-conventions'
def_node_search :graphql_name?, <<~PATTERN
(send nil? :graphql_name ...)
PATTERN
def on_class(node)
return unless graphql_name?(node)
return if node.body.single_line?
add_offense(node, location: :expression) unless graphql_name?(node.body.children.first)
end
end
end
end
end

View File

@ -35,7 +35,7 @@ describe('Contributors Store Getters', () => {
{ author_name: 'Carlson', author_email: 'carlson123@gmail.com', date: '2019-05-05' },
{ author_name: 'John', author_email: 'jawnnypoo@gmail.com', date: '2019-04-04' },
{ author_name: 'Johan', author_email: 'jawnnypoo@gmail.com', date: '2019-04-04' },
{ author_name: 'John', author_email: 'jawnnypoo@gmail.com', date: '2019-03-03' },
{ author_name: 'John', author_email: 'JAWNNYPOO@gmail.com', date: '2019-03-03' },
];
parsed = getters.parsedData(state);
});

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['WorkItem'] do
specify { expect(described_class.graphql_name).to eq('WorkItem') }
it 'has specific fields' do
fields = %i[description description_html id iid state title title_html work_item_type]
fields.each do |field_name|
expect(described_class).to have_graphql_fields(*fields)
end
end
end

View File

@ -1,21 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Sidebars::Concerns::WorkItemHierarchy do
shared_examples 'hierarchy menu' do
let(:item_id) { :hierarchy }
specify { is_expected.not_to be_nil }
end
describe 'Project hierarchy menu item' do
let_it_be_with_reload(:project) { create(:project, :repository) }
let(:user) { project.owner }
let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project) }
subject { Sidebars::Projects::Menus::ProjectInformationMenu.new(context).renderable_items.index { |e| e.item_id == item_id } }
it_behaves_like 'hierarchy menu'
end
end

View File

@ -59,11 +59,5 @@ RSpec.describe Sidebars::Projects::Menus::ProjectInformationMenu do
specify { is_expected.to be_nil }
end
end
describe 'Hierarchy' do
let(:item_id) { :hierarchy }
specify { is_expected.not_to be_nil }
end
end
end

View File

@ -0,0 +1,87 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe "Create a work item from a task in a work item's description" do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
let_it_be(:developer) { create(:user).tap { |user| project.add_developer(user) } }
let_it_be(:work_item, refind: true) { create(:work_item, project: project, description: '- [ ] A task in a list', lock_version: 3) }
let(:lock_version) { work_item.lock_version }
let(:input) do
{
'id' => work_item.to_global_id.to_s,
'workItemData' => {
'title' => 'A task in a list',
'workItemTypeId' => WorkItems::Type.default_by_type(:task).to_global_id.to_s,
'lineNumberStart' => 1,
'lineNumberEnd' => 1,
'lockVersion' => lock_version
}
}
end
let(:mutation) { graphql_mutation(:workItemCreateFromTask, input) }
let(:mutation_response) { graphql_mutation_response(:work_item_create_from_task) }
context 'the user is not allowed to update a work item' do
let(:current_user) { create(:user) }
it_behaves_like 'a mutation that returns a top-level access error'
end
context 'when user has permissions to create a work item' do
let(:current_user) { developer }
it 'creates the work item' do
expect do
post_graphql_mutation(mutation, current_user: current_user)
end.to change(WorkItem, :count).by(1)
created_work_item = WorkItem.last
work_item.reload
expect(response).to have_gitlab_http_status(:success)
expect(work_item.description).to eq("- [ ] #{created_work_item.to_reference}+")
expect(created_work_item.issue_type).to eq('task')
expect(created_work_item.work_item_type.base_type).to eq('task')
expect(mutation_response['workItem']).to include('id' => work_item.to_global_id.to_s)
expect(mutation_response['newWorkItem']).to include('id' => created_work_item.to_global_id.to_s)
end
context 'when creating a work item fails' do
let(:lock_version) { 2 }
it 'makes no changes to the DB and returns an error message' do
expect do
post_graphql_mutation(mutation, current_user: current_user)
work_item.reload
end.to not_change(WorkItem, :count).and(
not_change(work_item, :description)
)
expect(mutation_response['errors']).to contain_exactly('Stale work item. Check lock version')
end
end
it_behaves_like 'has spam protection' do
let(:mutation_class) { ::Mutations::WorkItems::CreateFromTask }
end
context 'when the work_items feature flag is disabled' do
before do
stub_feature_flags(work_items: false)
end
it 'does nothing and returns and error' do
expect do
post_graphql_mutation(mutation, current_user: current_user)
end.to not_change(WorkItem, :count)
expect(mutation_response['errors']).to contain_exactly('`work_items` feature flag disabled for this project')
end
end
end
end

View File

@ -0,0 +1,44 @@
# frozen_string_literal: true
require 'fast_spec_helper'
require_relative '../../../../rubocop/cop/graphql/graphql_name_position'
RSpec.describe RuboCop::Cop::Graphql::GraphqlNamePosition do
subject(:cop) { described_class.new }
it 'adds an offense when graphql_name is not on the first line' do
expect_offense(<<~TYPE)
module Types
class AType < BaseObject
^^^^^^^^^^^^^^^^^^^^^^^^ `graphql_name` should be the first line of the class: https://docs.gitlab.com/ee/development/api_graphql_styleguide.html#naming-conventions
field :a_thing
field :another_thing
graphql_name 'ATypeName'
end
end
TYPE
end
it 'does not add an offense for classes that have no call to graphql_name' do
expect_no_offenses(<<~TYPE.strip)
module Types
class AType < BaseObject
authorize :an_ability, :second_ability
field :a_thing
end
end
TYPE
end
it 'does not add an offense for classes that only call graphql_name' do
expect_no_offenses(<<~TYPE.strip)
module Types
class AType < BaseObject
graphql_name 'ATypeName'
end
end
TYPE
end
end

View File

@ -0,0 +1,96 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe WorkItems::CreateAndLinkService do
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
let_it_be(:user) { create(:user) }
let_it_be(:related_work_item) { create(:work_item, project: project) }
let(:spam_params) { double }
let(:link_params) { {} }
let(:params) do
{
title: 'Awesome work item',
description: 'please fix'
}
end
before_all do
project.add_developer(user)
end
describe '#execute' do
subject(:service_result) { described_class.new(project: project, current_user: user, params: params, spam_params: spam_params, link_params: link_params).execute }
before do
stub_spam_services
end
context 'when work item params are valid' do
it { is_expected.to be_success }
it 'creates a work item successfully with no links' do
expect do
service_result
end.to change(WorkItem, :count).by(1).and(
not_change(IssueLink, :count)
)
end
context 'when link params are valid' do
let(:link_params) { { issuable_references: [related_work_item.to_reference] } }
it 'creates a work item successfully with links' do
expect do
service_result
end.to change(WorkItem, :count).by(1).and(
change(IssueLink, :count).by(1)
)
end
end
context 'when link params are invalid' do
let(:link_params) { { issuable_references: ['invalid reference'] } }
it { is_expected.to be_error }
it 'does not create a link and does not rollback transaction' do
expect do
service_result
end.to not_change(IssueLink, :count).and(
change(WorkItem, :count).by(1)
)
end
it 'returns a link creation error message' do
expect(service_result.errors).to contain_exactly('No matching issue found. Make sure that you are adding a valid issue URL.')
end
end
end
context 'when work item params are invalid' do
let(:params) do
{
title: '',
description: 'invalid work item'
}
end
it { is_expected.to be_error }
it 'does not create a work item or links' do
expect do
service_result
end.to not_change(WorkItem, :count).and(
not_change(IssueLink, :count)
)
end
it 'returns work item errors' do
expect(service_result.errors).to contain_exactly("Title can't be blank")
end
end
end
end

View File

@ -0,0 +1,97 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe WorkItems::CreateFromTaskService do
let_it_be(:project) { create(:project) }
let_it_be(:developer) { create(:user) }
let_it_be(:list_work_item, refind: true) { create(:work_item, project: project, description: "- [ ] Item to be converted\n second line\n third line") }
let(:work_item_to_update) { list_work_item }
let(:spam_params) { double }
let(:link_params) { {} }
let(:current_user) { developer }
let(:params) do
{
title: 'Awesome work item',
work_item_type_id: WorkItems::Type.default_by_type(:task).id,
line_number_start: 1,
line_number_end: 3,
lock_version: work_item_to_update.lock_version
}
end
before_all do
project.add_developer(developer)
end
shared_examples 'CreateFromTask service with invalid params' do
it { is_expected.to be_error }
it 'does not create a work item or links' do
expect do
service_result
end.to not_change(WorkItem, :count).and(
not_change(IssueLink, :count)
)
end
end
describe '#execute' do
subject(:service_result) { described_class.new(work_item: work_item_to_update, current_user: current_user, work_item_params: params, spam_params: spam_params).execute }
before do
stub_spam_services
end
context 'when work item params are valid' do
it { is_expected.to be_success }
it 'creates a work item and links it to the original work item successfully' do
expect do
service_result
end.to change(WorkItem, :count).by(1).and(
change(IssueLink, :count)
)
end
it 'replaces the original issue markdown description with new work item reference' do
service_result
created_work_item = WorkItem.last
expect(list_work_item.description).to eq("- [ ] #{created_work_item.to_reference}+")
end
end
context 'when last operation fails' do
before do
params.merge!(line_number_start: 0)
end
it 'rollbacks all operations' do
expect do
service_result
end.to not_change(WorkItem, :count).and(
not_change(IssueLink, :count)
)
end
it { is_expected.to be_error }
it 'returns an error message' do
expect(service_result.errors).to contain_exactly('line_number_start must be greater than 0')
end
end
context 'when work item params are invalid' do
let(:params) { { title: '' } }
it_behaves_like 'CreateFromTask service with invalid params'
it 'returns work item errors' do
expect(service_result.errors).to contain_exactly("Title can't be blank")
end
end
end
end

View File

@ -0,0 +1,106 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe WorkItems::TaskListReferenceReplacementService do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:single_line_work_item, refind: true) { create(:work_item, project: project, description: '- [ ] single line', lock_version: 3) }
let_it_be(:multiple_line_work_item, refind: true) { create(:work_item, project: project, description: "Any text\n\n* [ ] Item to be converted\n second line\n third line", lock_version: 3) }
let(:line_number_start) { 3 }
let(:line_number_end) { 5 }
let(:title) { 'work item title' }
let(:reference) { 'any reference' }
let(:work_item) { multiple_line_work_item }
let(:lock_version) { 3 }
let(:expected_additional_text) { '' }
shared_examples 'successful work item task reference replacement service' do
it { is_expected.to be_success }
it 'replaces the original issue markdown description with new work item reference' do
result
expect(work_item.description).to eq("#{expected_additional_text}#{task_prefix} #{reference}+")
end
end
shared_examples 'failing work item task reference replacement service' do |error_message|
it { is_expected.to be_error }
it 'returns an error message' do
expect(result.errors).to contain_exactly(error_message)
end
end
describe '#execute' do
subject(:result) do
described_class.new(
work_item: work_item,
work_item_reference: reference,
line_number_start: line_number_start,
line_number_end: line_number_end,
title: title,
lock_version: lock_version
).execute
end
context 'when task mardown spans a single line' do
let(:line_number_start) { 1 }
let(:line_number_end) { 1 }
let(:work_item) { single_line_work_item }
let(:task_prefix) { '- [ ]' }
it_behaves_like 'successful work item task reference replacement service'
end
context 'when task mardown spans multiple lines' do
let(:task_prefix) { '* [ ]' }
let(:expected_additional_text) { "Any text\n\n" }
it_behaves_like 'successful work item task reference replacement service'
end
context 'when description does not contain a task' do
let_it_be(:no_matching_work_item) { create(:work_item, project: project, description: 'no matching task') }
let(:work_item) { no_matching_work_item }
it_behaves_like 'failing work item task reference replacement service', 'Unable to detect a task on line 3'
end
context 'when description is empty' do
let_it_be(:empty_work_item) { create(:work_item, project: project, description: '') }
let(:work_item) { empty_work_item }
it_behaves_like 'failing work item task reference replacement service', "Work item description can't be blank"
end
context 'when line_number_start is lower than 1' do
let(:line_number_start) { 0 }
it_behaves_like 'failing work item task reference replacement service', 'line_number_start must be greater than 0'
end
context 'when line_number_end is lower than line_number_start' do
let(:line_number_end) { line_number_start - 1 }
it_behaves_like 'failing work item task reference replacement service', 'line_number_end must be greater or equal to line_number_start'
end
context 'when lock_version is older than current' do
let(:lock_version) { 2 }
it_behaves_like 'failing work item task reference replacement service', 'Stale work item. Check lock version'
end
context 'when work item is stale before updating' do
it_behaves_like 'failing work item task reference replacement service', 'Stale work item. Check lock version' do
before do
::WorkItem.where(id: work_item.id).update_all(lock_version: lock_version + 1)
end
end
end
end
end

View File

@ -22,7 +22,6 @@ RSpec.shared_context 'project navbar structure' do
nav_sub_items: [
_('Activity'),
_('Labels'),
_('Planning hierarchy'),
_('Members')
]
},