diff --git a/app/assets/javascripts/contributors/stores/getters.js b/app/assets/javascripts/contributors/stores/getters.js index 45b569066f8..79f5c701fb8 100644 --- a/app/assets/javascripts/contributors/stores/getters.js +++ b/app/assets/javascripts/contributors/stores/getters.js @@ -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: { diff --git a/app/graphql/mutations/work_items/create_from_task.rb b/app/graphql/mutations/work_items/create_from_task.rb new file mode 100644 index 00000000000..bc79cc0f2ff --- /dev/null +++ b/app/graphql/mutations/work_items/create_from_task.rb @@ -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 diff --git a/app/graphql/types/base_enum.rb b/app/graphql/types/base_enum.rb index d70236f16f9..7e187d4d816 100644 --- a/app/graphql/types/base_enum.rb +++ b/app/graphql/types/base_enum.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +# rubocop:disable Graphql/GraphqlNamePosition module Types class BaseEnum < GraphQL::Schema::Enum class CustomValue < GraphQL::Schema::EnumValue diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb index 3c735231595..9efbde2da6b 100644 --- a/app/graphql/types/mutation_type.rb +++ b/app/graphql/types/mutation_type.rb @@ -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 diff --git a/app/graphql/types/work_items/convert_task_input_type.rb b/app/graphql/types/work_items/convert_task_input_type.rb new file mode 100644 index 00000000000..1f142c6815c --- /dev/null +++ b/app/graphql/types/work_items/convert_task_input_type.rb @@ -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 diff --git a/app/services/work_items/create_and_link_service.rb b/app/services/work_items/create_and_link_service.rb new file mode 100644 index 00000000000..534d220a846 --- /dev/null +++ b/app/services/work_items/create_and_link_service.rb @@ -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 diff --git a/app/services/work_items/create_from_task_service.rb b/app/services/work_items/create_from_task_service.rb new file mode 100644 index 00000000000..4203c96e676 --- /dev/null +++ b/app/services/work_items/create_from_task_service.rb @@ -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 diff --git a/app/services/work_items/task_list_reference_replacement_service.rb b/app/services/work_items/task_list_reference_replacement_service.rb new file mode 100644 index 00000000000..1044a4feb88 --- /dev/null +++ b/app/services/work_items/task_list_reference_replacement_service.rb @@ -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 diff --git a/data/deprecations/14-8-system_monitoring.yml b/data/deprecations/14-8-system_monitoring.yml new file mode 100644 index 00000000000..f285173eaf4 --- /dev/null +++ b/data/deprecations/14-8-system_monitoring.yml @@ -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 diff --git a/doc/administration/packages/container_registry.md b/doc/administration/packages/container_registry.md index 33a5311709f..435aa8fd10c 100644 --- a/doc/administration/packages/container_registry.md +++ b/doc/administration/packages/container_registry.md @@ -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() + +# 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 +``` diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 18c99b7d151..6fd876193b1 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -5178,6 +5178,29 @@ Input type: `WorkItemCreateInput` | `errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | | `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 | +| ---- | ---- | ----------- | +| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| `id` | [`WorkItemID!`](#workitemid) | Global ID of the work item. | +| `workItemData` | [`WorkItemConvertTaskInput!`](#workitemconverttaskinput) | Arguments necessary to convert a task into a work item. | + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| `errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | +| `newWorkItem` | [`WorkItem`](#workitem) | New work item created from task. | +| `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 | | ---- | ---- | ----------- | | `name` | [`String!`](#string) | Name of the vendor/maintainer. | + +### `WorkItemConvertTaskInput` + +#### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `lineNumberEnd` | [`Int!`](#int) | Last line in the Markdown source that defines the list item task. | +| `lineNumberStart` | [`Int!`](#int) | First line in the Markdown source that defines the list item task. | +| `lockVersion` | [`Int!`](#int) | Current lock version of the work item containing the task in the description. | +| `title` | [`String!`](#string) | Full string of the task to be replaced. New title for the created work item. | +| `workItemTypeId` | [`WorkItemsTypeID!`](#workitemstypeid) | Global ID of the work item type used to create the new work item. | diff --git a/doc/development/api_graphql_styleguide.md b/doc/development/api_graphql_styleguide.md index afd745533c9..417ccba26a0 100644 --- a/doc/development/api_graphql_styleguide.md +++ b/doc/development/api_graphql_styleguide.md @@ -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 diff --git a/doc/operations/error_tracking.md b/doc/operations/error_tracking.md index 6f97f002a32..738581fd040 100644 --- a/doc/operations/error_tracking.md +++ b/doc/operations/error_tracking.md @@ -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**. diff --git a/doc/update/deprecations.md b/doc/update/deprecations.md index 2f5146b8161..d05e1626c6c 100644 --- a/doc/update/deprecations.md +++ b/doc/update/deprecations.md @@ -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: diff --git a/lib/sidebars/concerns/work_item_hierarchy.rb b/lib/sidebars/concerns/work_item_hierarchy.rb deleted file mode 100644 index a4153bb5120..00000000000 --- a/lib/sidebars/concerns/work_item_hierarchy.rb +++ /dev/null @@ -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 diff --git a/lib/sidebars/projects/menus/project_information_menu.rb b/lib/sidebars/projects/menus/project_information_menu.rb index 4056d50d324..44b94ee3522 100644 --- a/lib/sidebars/projects/menus/project_information_menu.rb +++ b/lib/sidebars/projects/menus/project_information_menu.rb @@ -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 diff --git a/qa/qa/specs/features/browser_ui/5_package/container_registry/container_registry_omnibus_spec.rb b/qa/qa/specs/features/browser_ui/5_package/container_registry/container_registry_omnibus_spec.rb index e3c06242a9b..3aa54f1778d 100644 --- a/qa/qa/specs/features/browser_ui/5_package/container_registry/container_registry_omnibus_spec.rb +++ b/qa/qa/specs/features/browser_ui/5_package/container_registry/container_registry_omnibus_spec.rb @@ -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: diff --git a/rubocop/cop/graphql/graphql_name_position.rb b/rubocop/cop/graphql/graphql_name_position.rb new file mode 100644 index 00000000000..f18d65588cc --- /dev/null +++ b/rubocop/cop/graphql/graphql_name_position.rb @@ -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 diff --git a/spec/frontend/contributors/store/getters_spec.js b/spec/frontend/contributors/store/getters_spec.js index a4202e0ef4b..48218ff60e4 100644 --- a/spec/frontend/contributors/store/getters_spec.js +++ b/spec/frontend/contributors/store/getters_spec.js @@ -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); }); diff --git a/spec/graphql/types/work_item_type_spec.rb b/spec/graphql/types/work_item_type_spec.rb new file mode 100644 index 00000000000..d0bab477f36 --- /dev/null +++ b/spec/graphql/types/work_item_type_spec.rb @@ -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 diff --git a/spec/lib/sidebars/concerns/work_item_hierarchy_spec.rb b/spec/lib/sidebars/concerns/work_item_hierarchy_spec.rb deleted file mode 100644 index 2120341bf23..00000000000 --- a/spec/lib/sidebars/concerns/work_item_hierarchy_spec.rb +++ /dev/null @@ -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 diff --git a/spec/lib/sidebars/projects/menus/project_information_menu_spec.rb b/spec/lib/sidebars/projects/menus/project_information_menu_spec.rb index 24625413ded..7ff06ac229e 100644 --- a/spec/lib/sidebars/projects/menus/project_information_menu_spec.rb +++ b/spec/lib/sidebars/projects/menus/project_information_menu_spec.rb @@ -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 diff --git a/spec/requests/api/graphql/mutations/work_items/create_from_task_spec.rb b/spec/requests/api/graphql/mutations/work_items/create_from_task_spec.rb new file mode 100644 index 00000000000..8d33f8e1806 --- /dev/null +++ b/spec/requests/api/graphql/mutations/work_items/create_from_task_spec.rb @@ -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 diff --git a/spec/rubocop/cop/graphql/graphql_name_position_spec.rb b/spec/rubocop/cop/graphql/graphql_name_position_spec.rb new file mode 100644 index 00000000000..42cc398ed84 --- /dev/null +++ b/spec/rubocop/cop/graphql/graphql_name_position_spec.rb @@ -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 diff --git a/spec/services/work_items/create_and_link_service_spec.rb b/spec/services/work_items/create_and_link_service_spec.rb new file mode 100644 index 00000000000..93c029bdab1 --- /dev/null +++ b/spec/services/work_items/create_and_link_service_spec.rb @@ -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 diff --git a/spec/services/work_items/create_from_task_service_spec.rb b/spec/services/work_items/create_from_task_service_spec.rb new file mode 100644 index 00000000000..b4db925f053 --- /dev/null +++ b/spec/services/work_items/create_from_task_service_spec.rb @@ -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 diff --git a/spec/services/work_items/task_list_reference_replacement_service_spec.rb b/spec/services/work_items/task_list_reference_replacement_service_spec.rb new file mode 100644 index 00000000000..e7914eb4a92 --- /dev/null +++ b/spec/services/work_items/task_list_reference_replacement_service_spec.rb @@ -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 diff --git a/spec/support/shared_contexts/navbar_structure_context.rb b/spec/support/shared_contexts/navbar_structure_context.rb index 576a8aa44fa..27967850389 100644 --- a/spec/support/shared_contexts/navbar_structure_context.rb +++ b/spec/support/shared_contexts/navbar_structure_context.rb @@ -22,7 +22,6 @@ RSpec.shared_context 'project navbar structure' do nav_sub_items: [ _('Activity'), _('Labels'), - _('Planning hierarchy'), _('Members') ] },