Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
a4dd029f24
commit
129d7ea3db
|
|
@ -160,9 +160,7 @@ gdk-qa-reliable:
|
|||
QA_RUN_TYPE: gdk-qa-blocking
|
||||
parallel: 10
|
||||
rules:
|
||||
- if: $CI_MERGE_REQUEST_LABELS =~ /devops::govern|devops::create|devops::verify|devops::manage|devops::data stores/
|
||||
- when: on_success
|
||||
allow_failure: true
|
||||
- when: always
|
||||
|
||||
gdk-qa-reliable-with-load-balancer:
|
||||
extends:
|
||||
|
|
|
|||
|
|
@ -125,7 +125,6 @@ export default {
|
|||
|
||||
<template>
|
||||
<li
|
||||
data-qa-selector="board_card"
|
||||
:class="[
|
||||
{
|
||||
'multi-select gl-bg-blue-50 gl-border-blue-200': multiSelectVisible,
|
||||
|
|
@ -141,7 +140,7 @@ export default {
|
|||
:data-item-iid="item.iid"
|
||||
:data-item-path="item.referencePath"
|
||||
:style="cardStyle"
|
||||
data-testid="board_card"
|
||||
data-testid="board-card"
|
||||
class="board-card gl-p-5 gl-rounded-base gl-line-height-normal gl-relative gl-mb-3"
|
||||
@click="toggleIssue($event)"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ export default {
|
|||
}"
|
||||
:data-list-id="list.id"
|
||||
class="board gl-display-inline-block gl-h-full gl-px-3 gl-vertical-align-top gl-white-space-normal is-expandable"
|
||||
data-qa-selector="board_list"
|
||||
data-testid="board-list"
|
||||
>
|
||||
<div
|
||||
class="board-inner gl-display-flex gl-flex-direction-column gl-relative gl-h-full gl-rounded-base gl-bg-gray-50"
|
||||
|
|
|
|||
|
|
@ -219,7 +219,7 @@ export default {
|
|||
<template>
|
||||
<div
|
||||
v-cloak
|
||||
data-qa-selector="boards_list"
|
||||
data-testid="boards-list"
|
||||
class="gl-flex-grow-1 gl-display-flex gl-flex-direction-column gl-min-h-0"
|
||||
>
|
||||
<gl-alert v-if="errorToDisplay" variant="danger" :dismissible="true" @dismiss="dismissError">
|
||||
|
|
|
|||
|
|
@ -140,7 +140,7 @@ export default {
|
|||
variant: this.buttonKind,
|
||||
disabled: this.submitDisabled,
|
||||
loading: this.isLoading,
|
||||
'data-qa-selector': 'save_changes_button',
|
||||
'data-testid': 'save-changes-button',
|
||||
},
|
||||
};
|
||||
},
|
||||
|
|
@ -324,7 +324,7 @@ export default {
|
|||
ref="name"
|
||||
v-model="board.name"
|
||||
class="form-control"
|
||||
data-qa-selector="board_name_field"
|
||||
data-testid="board-name-field"
|
||||
type="text"
|
||||
:placeholder="$options.i18n.titleFieldPlaceholder"
|
||||
@keyup.enter="submit"
|
||||
|
|
|
|||
|
|
@ -653,7 +653,7 @@ export default {
|
|||
<div
|
||||
v-show="!list.collapsed"
|
||||
class="board-list-component gl-relative gl-h-full gl-display-flex gl-flex-direction-column gl-min-h-0"
|
||||
data-qa-selector="board_list_cards_area"
|
||||
data-testid="board-list-cards-area"
|
||||
>
|
||||
<div
|
||||
v-if="loading"
|
||||
|
|
|
|||
|
|
@ -365,7 +365,6 @@ export default {
|
|||
}"
|
||||
:style="headerStyle"
|
||||
class="board-header gl-relative"
|
||||
data-qa-selector="board_list_header"
|
||||
data-testid="board-list-header"
|
||||
>
|
||||
<h3
|
||||
|
|
|
|||
|
|
@ -289,7 +289,6 @@ export default {
|
|||
v-if="showDropdown"
|
||||
block
|
||||
data-testid="boards-dropdown"
|
||||
data-qa-selector="boards_dropdown"
|
||||
searchable
|
||||
:searching="loading"
|
||||
toggle-class="gl-min-w-20"
|
||||
|
|
@ -322,7 +321,7 @@ export default {
|
|||
block
|
||||
class="gl-justify-content-start!"
|
||||
category="tertiary"
|
||||
data-qa-selector="create_new_board_button"
|
||||
data-testid="create-new-board-button"
|
||||
data-track-action="click_button"
|
||||
data-track-label="create_new_board"
|
||||
data-track-property="dropdown"
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ export default {
|
|||
v-gl-tooltip
|
||||
:title="tooltipTitle"
|
||||
:class="{ 'dot-highlight': hasScope || boardHasScope }"
|
||||
data-qa-selector="boards_config_button"
|
||||
data-testid="boards-config-button"
|
||||
@click.prevent="showPage"
|
||||
>
|
||||
{{ buttonText }}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ export default {
|
|||
v-gl-tooltip
|
||||
category="tertiary"
|
||||
:icon="isFullscreen ? 'minimize' : 'maximize'"
|
||||
data-qa-selector="focus_mode_button"
|
||||
data-testid="focus-mode-button"
|
||||
:title="$options.i18n.toggleFocusMode"
|
||||
:aria-label="$options.i18n.toggleFocusMode"
|
||||
@click="toggleFocusMode"
|
||||
|
|
|
|||
|
|
@ -71,8 +71,7 @@ export default {
|
|||
size="small"
|
||||
class="dropdown-header-button gl-p-0!"
|
||||
icon="close"
|
||||
data-testid="close-button"
|
||||
data-qa-selector="close_labels_dropdown_button"
|
||||
data-testid="close-labels-dropdown-button"
|
||||
@click="$emit('closeDropdown')"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -144,7 +144,7 @@ export default {
|
|||
</slot>
|
||||
</template>
|
||||
<slot name="default">
|
||||
<gl-dropdown-form class="gl-relative gl-min-h-7" data-qa-selector="labels_dropdown_content">
|
||||
<gl-dropdown-form class="gl-relative gl-min-h-7" data-testid="labels-dropdown-content">
|
||||
<gl-loading-icon
|
||||
v-if="isLoading"
|
||||
size="lg"
|
||||
|
|
|
|||
|
|
@ -663,12 +663,6 @@ class Group < Namespace
|
|||
.non_invite
|
||||
end
|
||||
|
||||
def users_with_parents
|
||||
User
|
||||
.where(id: members_with_parents.select(:user_id))
|
||||
.reorder(nil)
|
||||
end
|
||||
|
||||
def users_with_descendants
|
||||
User
|
||||
.where(id: members_with_descendants.select(:user_id))
|
||||
|
|
|
|||
|
|
@ -12,17 +12,19 @@ module WorkItems
|
|||
end
|
||||
|
||||
def self.quick_action_commands
|
||||
[:set_parent]
|
||||
[:set_parent, :add_child]
|
||||
end
|
||||
|
||||
def self.quick_action_params
|
||||
[:set_parent]
|
||||
[:set_parent, :add_child]
|
||||
end
|
||||
|
||||
def self.process_quick_action_param(param_name, value)
|
||||
return super unless param_name == :set_parent && value
|
||||
return super unless param_name.in?(quick_action_params) && value.present?
|
||||
|
||||
{ parent: value }
|
||||
return { parent: value } if param_name == :set_parent
|
||||
|
||||
return { children: value } if param_name == :add_child
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: fetch_commits_for_bitbucket_server
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/133606
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/427699
|
||||
milestone: '16.5'
|
||||
type: development
|
||||
group: group::import and integrate
|
||||
default_enabled: false
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
key_path: redis_hll_counters.quickactions.i_quickactions_add_child_monthly
|
||||
name: quickactions_add_child_monthly
|
||||
description: Count of MAU using the `/add_child` quick action
|
||||
product_section: dev
|
||||
product_stage: plan
|
||||
product_group: product_planning
|
||||
value_type: number
|
||||
status: active
|
||||
milestone: "16.5"
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/132761
|
||||
time_frame: 28d
|
||||
data_source: redis_hll
|
||||
data_category: optional
|
||||
instrumentation_class: RedisHLLMetric
|
||||
options:
|
||||
events:
|
||||
- i_quickactions_add_child
|
||||
performance_indicator_type: []
|
||||
distribution:
|
||||
- ce
|
||||
- ee
|
||||
tier:
|
||||
- free
|
||||
- premium
|
||||
- ultimate
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
key_path: redis_hll_counters.quickactions.i_quickactions_set_parent_monthly
|
||||
name: quickactions_set_parent_monthly
|
||||
description: Count of WAU using the `/set_parent` quick action
|
||||
description: Count of MAU using the `/set_parent` quick action
|
||||
product_section: dev
|
||||
product_stage: plan
|
||||
product_group: product_planning
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
key_path: redis_hll_counters.quickactions.i_quickactions_add_child_weekly
|
||||
name: quickactions_add_child_weekly
|
||||
description: Count of WAU using the `/add_child` quick action
|
||||
product_section: dev
|
||||
product_stage: plan
|
||||
product_group: product_planning
|
||||
value_type: number
|
||||
status: active
|
||||
milestone: "16.5"
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/132761
|
||||
time_frame: 7d
|
||||
data_source: redis_hll
|
||||
data_category: optional
|
||||
instrumentation_class: RedisHLLMetric
|
||||
options:
|
||||
events:
|
||||
- i_quickactions_add_child
|
||||
performance_indicator_type: []
|
||||
distribution:
|
||||
- ce
|
||||
- ee
|
||||
tier:
|
||||
- free
|
||||
- premium
|
||||
- ultimate
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
- title: "Security policy field `newly_detected` is deprecated" # (required) Clearly explain the change, or planned change. For example, "The `confidential` field for a `Note` is deprecated" or "CI/CD job names will be limited to 250 characters."
|
||||
removal_milestone: "17.0" # (required) The milestone when this feature is planned to be removed
|
||||
announcement_milestone: "16.5" # (required) The milestone when this feature was first announced as deprecated.
|
||||
breaking_change: true # (required) Change to false if this is not a breaking change.
|
||||
reporter: g.hickman # (required) GitLab username of the person reporting the change
|
||||
stage: govern # (required) String value of the stage that the feature was created in. e.g., Growth
|
||||
issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/422414 # (required) Link to the deprecation issue in GitLab
|
||||
body: | # (required) Do not modify this line, instead modify the lines below.
|
||||
In [Support additional filters for scan result policies](https://gitlab.com/groups/gitlab-org/-/epics/6826#note_1341377224), we broke the `newly_detected` field into two options: `new_needs_triage` and `new_dismissed`. By including both options in the security policy YAML, you will achieve the same result as the original `newly_detected` field. However, you may now narrow your filter to ignore findings that have been dismissed by only using `new_needs_triage`.
|
||||
documentation_url: https://docs.gitlab.com/ee/user/application_security/policies/scan-result-policies.html#scan_finding-rule-type # (optional) This is a link to the current documentation page
|
||||
|
|
@ -844,6 +844,20 @@ Before upgrading to GitLab 17.0, please ensure you have [migrated](https://docs.
|
|||
|
||||
<div class="deprecation breaking-change" data-milestone="17.0">
|
||||
|
||||
### Security policy field `newly_detected` is deprecated
|
||||
|
||||
<div class="deprecation-notes">
|
||||
- Announced in GitLab <span class="milestone">16.5</span>
|
||||
- Removal in GitLab <span class="milestone">17.0</span> ([breaking change](https://docs.gitlab.com/ee/update/terminology.html#breaking-change))
|
||||
- To discuss this change or learn more, see the [deprecation issue](https://gitlab.com/gitlab-org/gitlab/-/issues/422414).
|
||||
</div>
|
||||
|
||||
In [Support additional filters for scan result policies](https://gitlab.com/groups/gitlab-org/-/epics/6826#note_1341377224), we broke the `newly_detected` field into two options: `new_needs_triage` and `new_dismissed`. By including both options in the security policy YAML, you will achieve the same result as the original `newly_detected` field. However, you may now narrow your filter to ignore findings that have been dismissed by only using `new_needs_triage`.
|
||||
|
||||
</div>
|
||||
|
||||
<div class="deprecation breaking-change" data-milestone="17.0">
|
||||
|
||||
### Self-managed certificate-based integration with Kubernetes
|
||||
|
||||
<div class="deprecation-notes">
|
||||
|
|
|
|||
|
|
@ -119,8 +119,6 @@ To view the value streams dashboard:
|
|||
|
||||
You can customize the Value Streams Dashboard and configure what subgroups and projects to include in the page.
|
||||
|
||||
A view can display maximum four subgroups or projects.
|
||||
|
||||
### Using query parameters
|
||||
|
||||
To display multiple subgroups and projects, specify their path as a URL parameter.
|
||||
|
|
|
|||
|
|
@ -230,8 +230,7 @@ table.supported-languages ul {
|
|||
<li>
|
||||
<a id="notes-regarding-supported-languages-and-package-managers-2"></a>
|
||||
<p>
|
||||
Java 21 LTS is only available when using <a href="https://maven.apache.org/">Maven</a> and is not supported when
|
||||
<a href="https://docs.gitlab.com/ee/development/fips_compliance.html#enable-fips-mode">FIPS mode</a> is enabled.
|
||||
Java 21 LTS is only available when using <a href="https://maven.apache.org/">Maven</a> or <a href="https://gradle.org/">Gradle</a>. Java 21 LTS for <a href="https://www.scala-sbt.org/">sbt</a> is not yet available and tracked in <a href="https://gitlab.com/gitlab-org/gitlab/-/issues/421174">issue 421174</a>. It is not supported when <a href="https://docs.gitlab.com/ee/development/fips_compliance.html#enable-fips-mode">FIPS mode</a> is enabled.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
|
|
|
|||
|
|
@ -38,6 +38,18 @@ Analyzers and their rules are updated [at least monthly](../index.md#vulnerabili
|
|||
The GitLab ruleset for the Semgrep-based analyzer is managed in [the GitLab-managed open-source `sast-rules` project](https://gitlab.com/gitlab-org/security-products/sast-rules).
|
||||
When rules are updated, they're released as part of the [Semgrep-based analyzer](https://gitlab.com/gitlab-org/security-products/analyzers/semgrep)'s container image.
|
||||
|
||||
### Rule update policies
|
||||
|
||||
Updates to SAST rules are not [breaking changes](../../../update/terminology.md#breaking-change).
|
||||
This means that rules may be added, removed, or updated without prior notice.
|
||||
|
||||
However, to make rule changes more convenient and understandable, GitLab:
|
||||
|
||||
- Documents [rule changes](#important-rule-changes) that are planned or completed.
|
||||
- [Automatically resolves](index.md#automatic-vulnerability-resolution) findings from rules after they are removed for Semgrep-based analyzers.
|
||||
- Enables you to [change the status on vulnerabilities where activity = "no longer detected" in bulk](../vulnerability_report/index.md#change-status-of-vulnerabilities).
|
||||
- Evaluates proposed rule changes for the impact they will have on existing vulnerability records.
|
||||
|
||||
## Configure rules in your projects
|
||||
|
||||
You should use the default SAST rules unless you have a specific reason to make a change.
|
||||
|
|
|
|||
|
|
@ -146,6 +146,7 @@ To auto-format this table, use the VS Code Markdown Table formatter: `https://do
|
|||
|:--------------------------------------------------------------|:-----------------------|:-----------------------|:-----------------------|:-------|
|
||||
| `/assign @user1 @user2` | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes | Assign one or more users. |
|
||||
| `/assign me` | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes | Assign yourself. |
|
||||
| `/add_child <work_item>` | **{dotted-circle}** No | **{check-circle}** Yes | **{dotted-circle}** No | Add child to `<work_item>`. The `<work_item>` value should be in the format of `#iid`, `group/project#iid`, or a URL to a work item. Multiple work items can be added as children at the same time. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/420797) in GitLab 16.5. |
|
||||
| `/award :emoji:` | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes | Toggle an emoji reaction. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/412275) in GitLab 16.5 |
|
||||
| `/cc @user` | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes | Mention a user. In GitLab 15.0 and later, this command performs no action. You can instead type `CC @user` or only `@user`. [In GitLab 14.9 and earlier](https://gitlab.com/gitlab-org/gitlab/-/issues/31200), mentioning a user at the start of a line creates a specific type of to-do item notification. |
|
||||
| `/checkin_reminder <cadence>` | **{dotted-circle}** No| **{check-circle}** Yes | **{dotted-circle}** No | Schedule [check-in reminders](../okrs.md#schedule-okr-check-in-reminders). Options are `weekly`, `twice-monthly`, `monthly`, or `never` (default). [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/422761) in GitLab 16.4 with flags named `okrs_mvc` and `okr_checkin_reminders`. |
|
||||
|
|
|
|||
|
|
@ -44,6 +44,10 @@ module BitbucketServer
|
|||
state == 'merged'
|
||||
end
|
||||
|
||||
def closed?
|
||||
state == 'closed'
|
||||
end
|
||||
|
||||
def created_at
|
||||
self.class.convert_timestamp(created_date)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -20,6 +20,22 @@ module Gitlab
|
|||
|
||||
break if pull_requests.empty?
|
||||
|
||||
commits_to_fetch = pull_requests.filter_map do |pull_request|
|
||||
next if already_processed?(pull_request)
|
||||
next unless pull_request.merged? || pull_request.closed?
|
||||
|
||||
[pull_request.source_branch_sha, pull_request.target_branch_sha]
|
||||
end.flatten
|
||||
|
||||
# Bitbucket Server keeps tracks of references for open pull requests in
|
||||
# refs/heads/pull-requests, but closed and merged requests get moved
|
||||
# into hidden internal refs under stash-refs/pull-requests. As a result,
|
||||
# they are not fetched by default.
|
||||
#
|
||||
# This method call explicitly fetches head and start commits for affected pull requests.
|
||||
# That allows us to correctly assign diffs and commits to merge requests.
|
||||
fetch_missing_commits(commits_to_fetch)
|
||||
|
||||
pull_requests.each do |pull_request|
|
||||
# Needs to come before `already_processed?` as `jobs_remaining` resets to zero when the job restarts and
|
||||
# jobs_remaining needs to be the total amount of enqueued jobs
|
||||
|
|
@ -42,6 +58,15 @@ module Gitlab
|
|||
|
||||
private
|
||||
|
||||
def fetch_missing_commits(commits_to_fetch)
|
||||
return if commits_to_fetch.blank?
|
||||
return unless Feature.enabled?(:fetch_commits_for_bitbucket_server, project.group)
|
||||
|
||||
project.repository.fetch_remote(project.import_url, refmap: commits_to_fetch, prune: false)
|
||||
rescue StandardError => e
|
||||
track_import_failure!(project, exception: e)
|
||||
end
|
||||
|
||||
def sidekiq_worker_class
|
||||
ImportPullRequestWorker
|
||||
end
|
||||
|
|
|
|||
|
|
@ -36,9 +36,21 @@ module Gitlab
|
|||
params 'Parent #iid, reference or URL'
|
||||
condition { supports_parent? && can_admin_link? }
|
||||
command :set_parent do |parent_param|
|
||||
@updates[:set_parent] = extract_work_item(parent_param)
|
||||
@updates[:set_parent] = extract_work_items(parent_param).first
|
||||
@execution_message[:set_parent] = success_msg[:set_parent]
|
||||
end
|
||||
|
||||
desc { _('Add children to work item') }
|
||||
explanation do |child_param|
|
||||
format(_("Add %{child_ref} to this work item as child(ren)."), child_ref: child_param)
|
||||
end
|
||||
types WorkItem
|
||||
params 'Children #iids, references or URLs'
|
||||
condition { supports_children? && can_admin_link? }
|
||||
command :add_child do |child_param|
|
||||
@updates[:add_child] = extract_work_items(child_param)
|
||||
@execution_message[:add_child] = success_msg[:add_child]
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
|
@ -64,15 +76,17 @@ module Gitlab
|
|||
nil
|
||||
end
|
||||
|
||||
def extract_work_item(params)
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def extract_work_items(params)
|
||||
return if params.nil?
|
||||
|
||||
issuable_type = params.include?('work_items') ? :work_item : :issue
|
||||
issuable = extract_references(params, issuable_type).first
|
||||
return unless issuable
|
||||
issuables = extract_references(params, issuable_type)
|
||||
return unless issuables
|
||||
|
||||
WorkItem.find(issuable.id)
|
||||
WorkItem.find(issuables.pluck(:id))
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
def validate_promote_to(type)
|
||||
return error_msg(:not_found, action: 'promote') unless type && supports_promote_to?(type.name)
|
||||
|
|
@ -111,7 +125,8 @@ module Gitlab
|
|||
{
|
||||
type: _('Type changed successfully.'),
|
||||
promote_to: _("Work item promoted successfully."),
|
||||
set_parent: _('Work item parent set successfully')
|
||||
set_parent: _('Work item parent set successfully'),
|
||||
add_child: _('Child work item(s) added successfully')
|
||||
}
|
||||
end
|
||||
|
||||
|
|
@ -119,6 +134,10 @@ module Gitlab
|
|||
::WorkItems::HierarchyRestriction.find_by_child_type_id(quick_action_target.work_item_type_id).present?
|
||||
end
|
||||
|
||||
def supports_children?
|
||||
::WorkItems::HierarchyRestriction.find_by_parent_type_id(quick_action_target.work_item_type_id).present?
|
||||
end
|
||||
|
||||
def can_admin_link?
|
||||
current_user.can?(:admin_issue_link, quick_action_target)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -2737,6 +2737,9 @@ msgstr ""
|
|||
msgid "Add \"%{value}\""
|
||||
msgstr ""
|
||||
|
||||
msgid "Add %{child_ref} to this work item as child(ren)."
|
||||
msgstr ""
|
||||
|
||||
msgid "Add %{linkStart}assets%{linkEnd} to your Release. GitLab automatically includes read-only assets, like source code and release evidence."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -2866,6 +2869,9 @@ msgstr ""
|
|||
msgid "Add child epic to an epic"
|
||||
msgstr ""
|
||||
|
||||
msgid "Add children to work item"
|
||||
msgstr ""
|
||||
|
||||
msgid "Add comment now"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -10047,6 +10053,9 @@ msgstr ""
|
|||
msgid "Child issues and epics"
|
||||
msgstr ""
|
||||
|
||||
msgid "Child work item(s) added successfully"
|
||||
msgstr ""
|
||||
|
||||
msgid "Chinese language support using"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -6,104 +6,112 @@ module QA
|
|||
module IssueBoard
|
||||
class Show < QA::Page::Base
|
||||
view 'app/assets/javascripts/boards/components/board_card.vue' do
|
||||
element :board_card
|
||||
element 'board-card'
|
||||
end
|
||||
|
||||
view 'app/assets/javascripts/boards/components/board_column.vue' do
|
||||
element 'board-list'
|
||||
end
|
||||
|
||||
view 'app/assets/javascripts/boards/components/board_form.vue' do
|
||||
element :board_name_field
|
||||
element :save_changes_button
|
||||
element 'board-name-field'
|
||||
element 'save-changes-button'
|
||||
end
|
||||
|
||||
view 'app/assets/javascripts/boards/components/board_list.vue' do
|
||||
element :board_list_cards_area
|
||||
element 'board-list-cards-area'
|
||||
end
|
||||
|
||||
view 'app/assets/javascripts/boards/components/board_list_header.vue' do
|
||||
element 'board-list-header'
|
||||
end
|
||||
|
||||
view 'app/assets/javascripts/boards/components/boards_selector.vue' do
|
||||
element :boards_dropdown
|
||||
element :create_new_board_button
|
||||
element 'boards-dropdown'
|
||||
element 'create-new-board-button'
|
||||
end
|
||||
|
||||
view 'app/assets/javascripts/boards/components/board_content.vue' do
|
||||
element :boards_list
|
||||
element 'boards-list'
|
||||
end
|
||||
|
||||
view 'app/assets/javascripts/boards/components/toggle_focus.vue' do
|
||||
element :focus_mode_button
|
||||
element 'focus-mode-button'
|
||||
end
|
||||
|
||||
view 'app/assets/javascripts/boards/components/config_toggle.vue' do
|
||||
element :boards_config_button
|
||||
element 'boards-config-button'
|
||||
end
|
||||
|
||||
# The `focused_board` method does not use `find_element` with an element defined
|
||||
# with the attribute `data-qa-selector` since such element is not unique when the
|
||||
# with the attribute `data-testid` since such element is not unique when the
|
||||
# `is-focused` class is not set, and it was not possible to find a better solution.
|
||||
def focused_board
|
||||
find('.issue-boards-content.js-focus-mode-board.is-focused')
|
||||
end
|
||||
|
||||
def boards_dropdown
|
||||
find_element(:boards_dropdown)
|
||||
find_element('boards-dropdown')
|
||||
end
|
||||
|
||||
def boards_list_cards_area_with_index(index)
|
||||
wait_boards_list_finish_loading do
|
||||
within_element_by_index(:board_list, index) do
|
||||
find_element(:board_list_cards_area)
|
||||
within_element_by_index('board-list', index) do
|
||||
find_element('board-list-cards-area')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def boards_list_header_with_index(index)
|
||||
wait_boards_list_finish_loading do
|
||||
within_element_by_index(:board_list, index) do
|
||||
find_element(:board_list_header)
|
||||
within_element_by_index('board-list', index) do
|
||||
find_element('board-list-header')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def card_of_list_with_index(index)
|
||||
wait_boards_list_finish_loading do
|
||||
within_element_by_index(:board_list, index) do
|
||||
find_element(:board_card)
|
||||
within_element_by_index('board-list', index) do
|
||||
find_element('board-card')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def click_boards_config_button
|
||||
click_element(:boards_config_button)
|
||||
click_element('boards-config-button')
|
||||
wait_for_requests
|
||||
end
|
||||
|
||||
def click_boards_dropdown_button
|
||||
# The dropdown button comes from the `GlDropdown` component of `@gitlab/ui`,
|
||||
# so it wasn't possible to add a `data-qa-selector` to it.
|
||||
find_element(:boards_dropdown).find('button').click
|
||||
find_element('boards-dropdown').find('button').click
|
||||
end
|
||||
|
||||
def click_focus_mode_button
|
||||
click_element(:focus_mode_button)
|
||||
click_element('focus-mode-button')
|
||||
end
|
||||
|
||||
def create_new_board(board_name)
|
||||
click_boards_dropdown_button
|
||||
click_element(:create_new_board_button)
|
||||
click_element('create-new-board-button')
|
||||
set_name(board_name)
|
||||
end
|
||||
|
||||
def has_modal_board_name_field?
|
||||
has_element?(:board_name_field, wait: 1)
|
||||
has_element?('board-name-field', wait: 1)
|
||||
end
|
||||
|
||||
def set_name(name)
|
||||
find_element(:board_name_field).set(name)
|
||||
click_element(:save_changes_button)
|
||||
find_element('board-name-field').set(name)
|
||||
click_element('save-changes-button')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def wait_boards_list_finish_loading
|
||||
within_element(:boards_list) do
|
||||
within_element('boards-list') do
|
||||
wait_until(reload: false, max_duration: 5, sleep_interval: 1) do
|
||||
finished_loading? && (block_given? ? yield : true)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -12,13 +12,18 @@ module QA
|
|||
tags: { import_type: ENV["QA_IMPORT_TYPE"], import_repo: ENV["QA_LARGE_IMPORT_REPO"] || "rspec/rspec-core" }
|
||||
} do
|
||||
describe 'Project import', product_group: :import_and_integrate do # rubocop:disable RSpec/MultipleMemoizedHelpers
|
||||
# Full object comparison is a fairly heavy operation
|
||||
# Importer itself returns counts of objects it fetched and counts it imported
|
||||
# We can use that for a lightweight comparison for very large projects
|
||||
let(:only_stats_comparison) { ENV["QA_LARGE_IMPORT_GH_ONLY_STATS_COMPARISON"] == "true" }
|
||||
let(:github_repo) { ENV['QA_LARGE_IMPORT_REPO'] || 'rspec/rspec-core' }
|
||||
let(:import_max_duration) { ENV['QA_LARGE_IMPORT_DURATION']&.to_i || 7200 }
|
||||
let(:api_parallel_threads) { ENV['QA_LARGE_IMPORT_API_PARALLEL']&.to_i || Etc.nprocessors }
|
||||
|
||||
let(:logger) { Runtime::Logger.logger }
|
||||
let(:differ) { RSpec::Support::Differ.new(color: true) }
|
||||
let(:gitlab_address) { QA::Runtime::Scenario.gitlab_address.chomp("/") }
|
||||
let(:dummy_url) { "https://example.com" }
|
||||
let(:dummy_url) { "https://example.com" } # this is used to replace all dynamic urls in descriptions and comments
|
||||
let(:api_request_params) { { auto_paginate: true, attempts: 2 } }
|
||||
|
||||
let(:created_by_pattern) { /\*Created by: \S+\*\n\n/ }
|
||||
|
|
@ -206,95 +211,88 @@ module QA
|
|||
|
||||
after do |example|
|
||||
unless defined?(@import_time)
|
||||
next save_json(
|
||||
"data",
|
||||
{
|
||||
status: "failed",
|
||||
importer: :github,
|
||||
import_finished: false,
|
||||
import_time: Time.now - @start,
|
||||
source: {
|
||||
name: "GitHub",
|
||||
project_name: github_repo,
|
||||
address: "https://github.com"
|
||||
},
|
||||
target: {
|
||||
name: "GitLab",
|
||||
address: gitlab_address
|
||||
}
|
||||
}
|
||||
)
|
||||
next save_data_json(test_result_data({
|
||||
status: "failed",
|
||||
importer: :github,
|
||||
import_finished: false,
|
||||
import_time: Time.now - @start
|
||||
}))
|
||||
end
|
||||
|
||||
# add additional import time metric
|
||||
example.metadata[:custom_test_metrics][:fields] = { import_time: @import_time }
|
||||
# save data for comparison notification creation
|
||||
save_json(
|
||||
"data",
|
||||
{
|
||||
if only_stats_comparison
|
||||
next save_data_json(test_result_data({
|
||||
status: example.exception ? "failed" : "passed",
|
||||
importer: :github,
|
||||
import_time: @import_time,
|
||||
import_finished: true,
|
||||
errors: imported_project.project_import_status[:failed_relations],
|
||||
reported_stats: @stats,
|
||||
source: {
|
||||
name: "GitHub",
|
||||
project_name: github_repo,
|
||||
address: "https://github.com",
|
||||
data: {
|
||||
branches: gh_branches.length,
|
||||
commits: gh_commits.length,
|
||||
labels: gh_labels.length,
|
||||
milestones: gh_milestones.length,
|
||||
mrs: gh_prs.length,
|
||||
mr_comments: gh_prs.sum { |_k, v| v[:comments].length },
|
||||
mr_events: gh_prs.sum { |_k, v| v[:events].length },
|
||||
issues: gh_issues.length,
|
||||
issue_comments: gh_issues.sum { |_k, v| v[:comments].length },
|
||||
issue_events: gh_issues.sum { |_k, v| v[:events].length }
|
||||
}
|
||||
},
|
||||
target: {
|
||||
name: "GitLab",
|
||||
project_name: imported_project.path_with_namespace,
|
||||
address: gitlab_address,
|
||||
data: {
|
||||
branches: gl_branches.length,
|
||||
commits: gl_commits.length,
|
||||
labels: gl_labels.length,
|
||||
milestones: gl_milestones.length,
|
||||
mrs: mrs.length,
|
||||
mr_comments: mrs.sum { |_k, v| v[:comments].length },
|
||||
mr_events: mrs.sum { |_k, v| v[:events].length },
|
||||
issues: gl_issues.length,
|
||||
issue_comments: gl_issues.sum { |_k, v| v[:comments].length },
|
||||
issue_events: gl_issues.sum { |_k, v| v[:events].length }
|
||||
}
|
||||
},
|
||||
not_imported: {
|
||||
mrs: @mr_diff,
|
||||
issues: @issue_diff
|
||||
reported_stats: @stats
|
||||
}))
|
||||
end
|
||||
|
||||
save_data_json(test_result_data({
|
||||
status: example.exception ? "failed" : "passed",
|
||||
import_time: @import_time,
|
||||
import_finished: true,
|
||||
errors: imported_project.project_import_status[:failed_relations],
|
||||
reported_stats: @stats,
|
||||
source: {
|
||||
data: {
|
||||
branches: gh_branches.length,
|
||||
commits: gh_commits.length,
|
||||
labels: gh_labels.length,
|
||||
milestones: gh_milestones.length,
|
||||
mrs: gh_prs.length,
|
||||
mr_comments: gh_prs.sum { |_k, v| v[:comments].length },
|
||||
mr_events: gh_prs.sum { |_k, v| v[:events].length },
|
||||
issues: gh_issues.length,
|
||||
issue_comments: gh_issues.sum { |_k, v| v[:comments].length },
|
||||
issue_events: gh_issues.sum { |_k, v| v[:events].length }
|
||||
}
|
||||
},
|
||||
target: {
|
||||
project_name: imported_project.path_with_namespace,
|
||||
data: {
|
||||
branches: gl_branches.length,
|
||||
commits: gl_commits.length,
|
||||
labels: gl_labels.length,
|
||||
milestones: gl_milestones.length,
|
||||
mrs: mrs.length,
|
||||
mr_comments: mrs.sum { |_k, v| v[:comments].length },
|
||||
mr_events: mrs.sum { |_k, v| v[:events].length },
|
||||
issues: gl_issues.length,
|
||||
issue_comments: gl_issues.sum { |_k, v| v[:comments].length },
|
||||
issue_events: gl_issues.sum { |_k, v| v[:events].length }
|
||||
}
|
||||
},
|
||||
not_imported: {
|
||||
mrs: @mr_diff,
|
||||
issues: @issue_diff
|
||||
}
|
||||
)
|
||||
}))
|
||||
end
|
||||
|
||||
it(
|
||||
'imports large Github repo via api',
|
||||
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347668'
|
||||
) do
|
||||
if only_stats_comparison
|
||||
logger.warn("Test is running in lightweight comparison mode, only object counts will be compared!")
|
||||
end
|
||||
|
||||
@start = Time.now
|
||||
|
||||
# trigger import and log project paths
|
||||
logger.info("== Triggering import of project '#{github_repo}' in to '#{imported_project.reload!.full_path}' ==")
|
||||
|
||||
# fetch all objects right after import has started
|
||||
fetch_github_objects
|
||||
fetch_github_objects unless only_stats_comparison
|
||||
|
||||
import_status = -> {
|
||||
imported_project.project_import_status.yield_self do |status|
|
||||
@stats = status.dig(:stats, :imported)
|
||||
@stats = status[:stats]&.slice(:fetched, :imported)
|
||||
|
||||
# fail fast if import explicitly failed
|
||||
raise "Import of '#{imported_project.full_path}' failed!" if status[:import_status] == 'failed'
|
||||
|
|
@ -308,15 +306,38 @@ module QA
|
|||
|
||||
@import_time = Time.now - @start
|
||||
|
||||
aggregate_failures do
|
||||
verify_repository_import
|
||||
verify_labels_import
|
||||
verify_milestones_import
|
||||
verify_merge_requests_import
|
||||
verify_issues_import
|
||||
if only_stats_comparison
|
||||
expect(@stats[:fetched]).to eq(@stats[:imported])
|
||||
else
|
||||
aggregate_failures do
|
||||
verify_repository_import
|
||||
verify_labels_import
|
||||
verify_milestones_import
|
||||
verify_merge_requests_import
|
||||
verify_issues_import
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Base test result data used for test result reporting
|
||||
#
|
||||
# @param [Hash] additional_data
|
||||
# @return [Hash]
|
||||
def test_result_data(additional_data = {})
|
||||
{
|
||||
importer: :github,
|
||||
source: {
|
||||
name: "GitHub",
|
||||
project_name: github_repo,
|
||||
address: "https://github.com"
|
||||
},
|
||||
target: {
|
||||
name: "GitLab",
|
||||
address: gitlab_address
|
||||
}
|
||||
}.deep_merge(additional_data)
|
||||
end
|
||||
|
||||
# Persist all objects from repository being imported
|
||||
#
|
||||
# @return [void]
|
||||
|
|
@ -634,11 +655,10 @@ module QA
|
|||
|
||||
# Save json as file
|
||||
#
|
||||
# @param [String] name
|
||||
# @param [Hash] json
|
||||
# @return [void]
|
||||
def save_json(name, json)
|
||||
File.open("tmp/#{name}.json", "w") { |file| file.write(JSON.pretty_generate(json)) }
|
||||
def save_data_json(json)
|
||||
File.open("tmp/github-import-data.json", "w") { |file| file.write(JSON.pretty_generate(json)) }
|
||||
end
|
||||
|
||||
# Extract id number from web url of issue or pull request
|
||||
|
|
|
|||
|
|
@ -78,7 +78,6 @@ module QA
|
|||
after do |example|
|
||||
unless defined?(@import_time)
|
||||
next save_json(
|
||||
"data",
|
||||
{
|
||||
status: "failed",
|
||||
importer: :gitlab,
|
||||
|
|
@ -101,7 +100,6 @@ module QA
|
|||
example.metadata[:custom_test_metrics][:fields] = { import_time: @import_time }
|
||||
# save data for comparison notification creation
|
||||
save_json(
|
||||
"data",
|
||||
{
|
||||
status: example.exception ? "failed" : "passed",
|
||||
importer: :gitlab,
|
||||
|
|
@ -429,11 +427,10 @@ module QA
|
|||
|
||||
# Save json as file
|
||||
#
|
||||
# @param [String] name
|
||||
# @param [Hash] json
|
||||
# @return [void]
|
||||
def save_json(name, json)
|
||||
File.open("tmp/#{name}.json", "w") { |file| file.write(JSON.pretty_generate(json)) }
|
||||
def save_json(json)
|
||||
File.open("tmp/gitlab-import-data.json", "w") { |file| file.write(JSON.pretty_generate(json)) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -153,6 +153,8 @@ begin
|
|||
|
||||
sleep 1
|
||||
end
|
||||
rescue Interrupt
|
||||
# Quietly shut down
|
||||
ensure
|
||||
print "\e[?1049l" # Restores the original screen buffer
|
||||
print "\e[H" # Moves the cursor home
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ RSpec.describe 'Issue boards sidebar labels select', :js, feature_category: :tea
|
|||
|
||||
include_context 'labels from nested groups and projects'
|
||||
|
||||
let(:card) { find('.board:nth-child(1)').first('[data-testid="board_card"]') }
|
||||
let(:card) { find('.board:nth-child(1)').first('[data-testid="board-card"]') }
|
||||
|
||||
context 'group boards' do
|
||||
context 'in the top-level group board' do
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ RSpec.describe 'Project issue boards sidebar', :js, feature_category: :team_plan
|
|||
it_behaves_like 'issue boards sidebar'
|
||||
|
||||
def first_card
|
||||
find('.board:nth-child(1)').first("[data-testid='board_card']")
|
||||
find('.board:nth-child(1)').first("[data-testid='board-card']")
|
||||
end
|
||||
|
||||
def click_first_issue_card
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ RSpec.describe 'User visits issue boards', :js, feature_category: :team_planning
|
|||
it 'displays all issues satisfiying filter params and correctly sets url params' do
|
||||
expect(page).to have_current_path(board_path)
|
||||
|
||||
page.assert_selector('[data-testid="board_card"]', count: expected_issues.length)
|
||||
page.assert_selector('[data-testid="board-card"]', count: expected_issues.length)
|
||||
expected_issues.each { |issue_title| expect(page).to have_link issue_title }
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -82,6 +82,18 @@ RSpec.describe BitbucketServer::Representation::PullRequest, feature_category: :
|
|||
it { expect(subject.merged?).to be_truthy }
|
||||
end
|
||||
|
||||
describe '#closed?' do
|
||||
it { expect(subject.closed?).to be_falsey }
|
||||
|
||||
context 'for declined pull requests' do
|
||||
before do
|
||||
sample_data['state'] = 'DECLINED'
|
||||
end
|
||||
|
||||
it { expect(subject.closed?).to be_truthy }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#created_at' do
|
||||
it { expect(subject.created_at.to_i).to eq(sample_data['createdDate'] / 1000) }
|
||||
end
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ require 'spec_helper'
|
|||
|
||||
RSpec.describe Gitlab::BitbucketServerImport::Importers::PullRequestsImporter, feature_category: :importers do
|
||||
let_it_be(:project) do
|
||||
create(:project, :import_started,
|
||||
create(:project, :with_import_url, :import_started, :empty_repo,
|
||||
import_data_attributes: {
|
||||
data: { 'project_key' => 'key', 'repo_slug' => 'slug' },
|
||||
credentials: { 'base_uri' => 'http://bitbucket.org/', 'user' => 'bitbucket', 'password' => 'password' }
|
||||
|
|
@ -19,8 +19,30 @@ RSpec.describe Gitlab::BitbucketServerImport::Importers::PullRequestsImporter, f
|
|||
allow_next_instance_of(BitbucketServer::Client) do |client|
|
||||
allow(client).to receive(:pull_requests).and_return(
|
||||
[
|
||||
BitbucketServer::Representation::PullRequest.new({ 'id' => 1 }),
|
||||
BitbucketServer::Representation::PullRequest.new({ 'id' => 2 })
|
||||
BitbucketServer::Representation::PullRequest.new(
|
||||
{
|
||||
'id' => 1,
|
||||
'state' => 'MERGED',
|
||||
'fromRef' => { 'latestCommit' => 'aaaa1' },
|
||||
'toRef' => { 'latestCommit' => 'aaaa2' }
|
||||
}
|
||||
),
|
||||
BitbucketServer::Representation::PullRequest.new(
|
||||
{
|
||||
'id' => 2,
|
||||
'state' => 'DECLINED',
|
||||
'fromRef' => { 'latestCommit' => 'bbbb1' },
|
||||
'toRef' => { 'latestCommit' => 'bbbb2' }
|
||||
}
|
||||
),
|
||||
BitbucketServer::Representation::PullRequest.new(
|
||||
{
|
||||
'id' => 3,
|
||||
'state' => 'OPEN',
|
||||
'fromRef' => { 'latestCommit' => 'cccc1' },
|
||||
'toRef' => { 'latestCommit' => 'cccc2' }
|
||||
}
|
||||
)
|
||||
],
|
||||
[]
|
||||
)
|
||||
|
|
@ -28,14 +50,14 @@ RSpec.describe Gitlab::BitbucketServerImport::Importers::PullRequestsImporter, f
|
|||
end
|
||||
|
||||
it 'imports each pull request in parallel', :aggregate_failures do
|
||||
expect(Gitlab::BitbucketServerImport::ImportPullRequestWorker).to receive(:perform_in).twice
|
||||
expect(Gitlab::BitbucketServerImport::ImportPullRequestWorker).to receive(:perform_in).thrice
|
||||
|
||||
waiter = importer.execute
|
||||
|
||||
expect(waiter).to be_an_instance_of(Gitlab::JobWaiter)
|
||||
expect(waiter.jobs_remaining).to eq(2)
|
||||
expect(waiter.jobs_remaining).to eq(3)
|
||||
expect(Gitlab::Cache::Import::Caching.values_from_set(importer.already_processed_cache_key))
|
||||
.to match_array(%w[1 2])
|
||||
.to match_array(%w[1 2 3])
|
||||
end
|
||||
|
||||
context 'when pull request was already processed' do
|
||||
|
|
@ -44,12 +66,68 @@ RSpec.describe Gitlab::BitbucketServerImport::Importers::PullRequestsImporter, f
|
|||
end
|
||||
|
||||
it 'does not schedule job for processed pull requests', :aggregate_failures do
|
||||
expect(Gitlab::BitbucketServerImport::ImportPullRequestWorker).to receive(:perform_in).once
|
||||
expect(Gitlab::BitbucketServerImport::ImportPullRequestWorker).to receive(:perform_in).twice
|
||||
|
||||
waiter = importer.execute
|
||||
|
||||
expect(waiter).to be_an_instance_of(Gitlab::JobWaiter)
|
||||
expect(waiter.jobs_remaining).to eq(2)
|
||||
expect(waiter.jobs_remaining).to eq(3)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when pull requests are in merged or declined status' do
|
||||
it 'fetches latest commits from the remote repository' do
|
||||
expect(project.repository).to receive(:fetch_remote).with(
|
||||
project.import_url,
|
||||
refmap: %w[aaaa1 aaaa2 bbbb1 bbbb2],
|
||||
prune: false
|
||||
)
|
||||
|
||||
importer.execute
|
||||
end
|
||||
|
||||
context 'when feature flag "fetch_commits_for_bitbucket_server" is disabled' do
|
||||
before do
|
||||
stub_feature_flags(fetch_commits_for_bitbucket_server: false)
|
||||
end
|
||||
|
||||
it 'does not fetch anything' do
|
||||
expect(project.repository).not_to receive(:fetch_remote)
|
||||
importer.execute
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there are no commits to process' do
|
||||
before do
|
||||
Gitlab::Cache::Import::Caching.set_add(importer.already_processed_cache_key, 1)
|
||||
Gitlab::Cache::Import::Caching.set_add(importer.already_processed_cache_key, 2)
|
||||
end
|
||||
|
||||
it 'does not fetch anything' do
|
||||
expect(project.repository).not_to receive(:fetch_remote)
|
||||
|
||||
importer.execute
|
||||
end
|
||||
end
|
||||
|
||||
context 'when fetch process is failed' do
|
||||
let(:exception) { ArgumentError.new('blank or empty URL') }
|
||||
|
||||
before do
|
||||
allow(project.repository).to receive(:fetch_remote).and_raise(exception)
|
||||
end
|
||||
|
||||
it 'rescues and logs the exception' do
|
||||
expect(Gitlab::Import::ImportFailureService)
|
||||
.to receive(:track)
|
||||
.with(
|
||||
project_id: project.id,
|
||||
exception: exception,
|
||||
error_source: described_class.name
|
||||
).and_call_original
|
||||
|
||||
importer.execute
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -334,6 +334,46 @@ RSpec.describe Notes::QuickActionsService, feature_category: :team_planning do
|
|||
end
|
||||
end
|
||||
|
||||
describe '/add_child' do
|
||||
let_it_be_with_reload(:noteable) { create(:work_item, :objective, project: project) }
|
||||
let_it_be_with_reload(:child) { create(:work_item, :objective, project: project) }
|
||||
let_it_be_with_reload(:second_child) { create(:work_item, :objective, project: project) }
|
||||
let_it_be(:note_text) { "/add_child #{child.to_reference}, #{second_child.to_reference}" }
|
||||
let_it_be(:note) { create(:note, noteable: noteable, project: project, note: note_text) }
|
||||
let_it_be(:children) { [child, second_child] }
|
||||
|
||||
shared_examples 'adds child work items' do
|
||||
it 'leaves the note empty' do
|
||||
expect(execute(note)).to be_empty
|
||||
end
|
||||
|
||||
it 'adds child work items' do
|
||||
execute(note)
|
||||
|
||||
expect(noteable.valid?).to be_truthy
|
||||
expect(noteable.work_item_children).to eq(children)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when using work item reference' do
|
||||
let_it_be(:note_text) { "/add_child #{child.to_reference(full: true)},#{second_child.to_reference(full: true)}" }
|
||||
|
||||
it_behaves_like 'adds child work items'
|
||||
end
|
||||
|
||||
context 'when using work item iid' do
|
||||
it_behaves_like 'adds child work items'
|
||||
end
|
||||
|
||||
context 'when using work item URL' do
|
||||
let_it_be(:project_path) { "#{Gitlab.config.gitlab.url}/#{project.full_path}" }
|
||||
let_it_be(:url) { "#{project_path}/work_items/#{child.iid},#{project_path}/work_items/#{second_child.iid}" }
|
||||
let_it_be(:note_text) { "/add_child #{url}" }
|
||||
|
||||
it_behaves_like 'adds child work items'
|
||||
end
|
||||
end
|
||||
|
||||
describe '/set_parent' do
|
||||
let_it_be_with_reload(:noteable) { create(:work_item, :objective, project: project) }
|
||||
let_it_be_with_reload(:parent) { create(:work_item, :objective, project: project) }
|
||||
|
|
|
|||
|
|
@ -3091,6 +3091,55 @@ RSpec.describe QuickActions::InterpretService, feature_category: :team_planning
|
|||
it_behaves_like 'command is not available'
|
||||
end
|
||||
end
|
||||
|
||||
describe '/add_child command' do
|
||||
let_it_be(:child) { create(:work_item, :issue, project: project) }
|
||||
let_it_be(:work_item) { create(:work_item, :objective, project: project) }
|
||||
let_it_be(:child_ref) { child.to_reference(project) }
|
||||
|
||||
let(:command) { "/add_child #{child_ref}" }
|
||||
|
||||
shared_examples 'command is available' do
|
||||
it 'explanation contains correct message' do
|
||||
_, explanations = service.explain(command, work_item)
|
||||
|
||||
expect(explanations)
|
||||
.to contain_exactly("Add #{child_ref} to this work item as child(ren).")
|
||||
end
|
||||
|
||||
it 'contains command' do
|
||||
expect(service.available_commands(work_item)).to include(a_hash_including(name: :add_child))
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'command is not available' do
|
||||
it 'explanation is empty' do
|
||||
_, explanations = service.explain(command, work_item)
|
||||
|
||||
expect(explanations).to eq([])
|
||||
end
|
||||
|
||||
it 'does not contain command' do
|
||||
expect(service.available_commands(work_item)).not_to include(a_hash_including(name: :add_child))
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user can admin link' do
|
||||
it_behaves_like 'command is available'
|
||||
|
||||
context 'when work item type does not support children' do
|
||||
let_it_be(:work_item) { build(:work_item, :key_result, project: project) }
|
||||
|
||||
it_behaves_like 'command is not available'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user cannot admin link' do
|
||||
subject(:service) { described_class.new(project, create(:user)) }
|
||||
|
||||
it_behaves_like 'command is not available'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#available_commands' do
|
||||
|
|
|
|||
Loading…
Reference in New Issue