Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab/ci/test-on-gdk/main.gitlab-ci.yml4
-rw-r--r--app/assets/javascripts/boards/components/board_card.vue3
-rw-r--r--app/assets/javascripts/boards/components/board_column.vue2
-rw-r--r--app/assets/javascripts/boards/components/board_content.vue2
-rw-r--r--app/assets/javascripts/boards/components/board_form.vue4
-rw-r--r--app/assets/javascripts/boards/components/board_list.vue2
-rw-r--r--app/assets/javascripts/boards/components/board_list_header.vue1
-rw-r--r--app/assets/javascripts/boards/components/boards_selector.vue3
-rw-r--r--app/assets/javascripts/boards/components/config_toggle.vue2
-rw-r--r--app/assets/javascripts/boards/components/toggle_focus.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_header.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue2
-rw-r--r--app/models/group.rb6
-rw-r--r--app/models/work_items/widgets/hierarchy.rb10
-rw-r--r--config/feature_flags/development/fetch_commits_for_bitbucket_server.yml8
-rw-r--r--config/metrics/counts_28d/20230927152527_i_quickactions_add_child_monthly.yml26
-rw-r--r--config/metrics/counts_7d/20230922165305_i_quickactions_set_parent_monthly.yml2
-rw-r--r--config/metrics/counts_7d/20230927152525_i_quickactions_add_child_weekly.yml26
-rw-r--r--data/deprecations/16-4-deprecate-newly-detected-field.yml10
-rw-r--r--doc/update/deprecations.md14
-rw-r--r--doc/user/analytics/value_streams_dashboard.md2
-rw-r--r--doc/user/application_security/dependency_scanning/index.md3
-rw-r--r--doc/user/application_security/sast/rules.md12
-rw-r--r--doc/user/project/quick_actions.md1
-rw-r--r--lib/bitbucket_server/representation/pull_request.rb4
-rw-r--r--lib/gitlab/bitbucket_server_import/importers/pull_requests_importer.rb25
-rw-r--r--lib/gitlab/quick_actions/work_item_actions.rb31
-rw-r--r--locale/gitlab.pot9
-rw-r--r--qa/qa/page/component/issue_board/show.rb58
-rw-r--r--qa/qa/specs/features/api/1_manage/import/import_large_github_repo_spec.rb166
-rw-r--r--qa/qa/specs/features/api/1_manage/migration/gitlab_migration_large_project_spec.rb7
-rw-r--r--scripts/internal_events/monitor.rb2
-rw-r--r--spec/features/boards/sidebar_labels_in_namespaces_spec.rb2
-rw-r--r--spec/features/boards/sidebar_spec.rb2
-rw-r--r--spec/features/boards/user_visits_board_spec.rb2
-rw-r--r--spec/lib/bitbucket_server/representation/pull_request_spec.rb12
-rw-r--r--spec/lib/gitlab/bitbucket_server_import/importers/pull_requests_importer_spec.rb94
-rw-r--r--spec/services/notes/quick_actions_service_spec.rb40
-rw-r--r--spec/services/quick_actions/interpret_service_spec.rb49
39 files changed, 500 insertions, 153 deletions
diff --git a/.gitlab/ci/test-on-gdk/main.gitlab-ci.yml b/.gitlab/ci/test-on-gdk/main.gitlab-ci.yml
index 9051532b0d1..a64dd450c82 100644
--- a/.gitlab/ci/test-on-gdk/main.gitlab-ci.yml
+++ b/.gitlab/ci/test-on-gdk/main.gitlab-ci.yml
@@ -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:
diff --git a/app/assets/javascripts/boards/components/board_card.vue b/app/assets/javascripts/boards/components/board_card.vue
index 05865dc7305..e68b10ae752 100644
--- a/app/assets/javascripts/boards/components/board_card.vue
+++ b/app/assets/javascripts/boards/components/board_card.vue
@@ -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)"
>
diff --git a/app/assets/javascripts/boards/components/board_column.vue b/app/assets/javascripts/boards/components/board_column.vue
index bcd7db8dcb4..67a4c5eba45 100644
--- a/app/assets/javascripts/boards/components/board_column.vue
+++ b/app/assets/javascripts/boards/components/board_column.vue
@@ -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"
diff --git a/app/assets/javascripts/boards/components/board_content.vue b/app/assets/javascripts/boards/components/board_content.vue
index 3c2659b00c9..554f3bfa416 100644
--- a/app/assets/javascripts/boards/components/board_content.vue
+++ b/app/assets/javascripts/boards/components/board_content.vue
@@ -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">
diff --git a/app/assets/javascripts/boards/components/board_form.vue b/app/assets/javascripts/boards/components/board_form.vue
index 6ea94e04e44..a3d55ac8306 100644
--- a/app/assets/javascripts/boards/components/board_form.vue
+++ b/app/assets/javascripts/boards/components/board_form.vue
@@ -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"
diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue
index 1bb7e88122a..2693a6bb5ea 100644
--- a/app/assets/javascripts/boards/components/board_list.vue
+++ b/app/assets/javascripts/boards/components/board_list.vue
@@ -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"
diff --git a/app/assets/javascripts/boards/components/board_list_header.vue b/app/assets/javascripts/boards/components/board_list_header.vue
index 3dd7f7b70f5..0235edd69ac 100644
--- a/app/assets/javascripts/boards/components/board_list_header.vue
+++ b/app/assets/javascripts/boards/components/board_list_header.vue
@@ -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
diff --git a/app/assets/javascripts/boards/components/boards_selector.vue b/app/assets/javascripts/boards/components/boards_selector.vue
index 7bc4c89699c..cd2a4a02b2e 100644
--- a/app/assets/javascripts/boards/components/boards_selector.vue
+++ b/app/assets/javascripts/boards/components/boards_selector.vue
@@ -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"
diff --git a/app/assets/javascripts/boards/components/config_toggle.vue b/app/assets/javascripts/boards/components/config_toggle.vue
index bc896932ffc..69e6cc870d2 100644
--- a/app/assets/javascripts/boards/components/config_toggle.vue
+++ b/app/assets/javascripts/boards/components/config_toggle.vue
@@ -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 }}
diff --git a/app/assets/javascripts/boards/components/toggle_focus.vue b/app/assets/javascripts/boards/components/toggle_focus.vue
index 990a6fa63d4..a886abf9e61 100644
--- a/app/assets/javascripts/boards/components/toggle_focus.vue
+++ b/app/assets/javascripts/boards/components/toggle_focus.vue
@@ -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"
diff --git a/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_header.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_header.vue
index 154a8e866d0..377200ab804 100644
--- a/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_header.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_header.vue
@@ -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>
diff --git a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue
index b34a6b11092..1f5896204ee 100644
--- a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue
+++ b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue
@@ -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"
diff --git a/app/models/group.rb b/app/models/group.rb
index 587451ac195..bc6125887d4 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -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))
diff --git a/app/models/work_items/widgets/hierarchy.rb b/app/models/work_items/widgets/hierarchy.rb
index b5d8a2ced45..9d90a56d59e 100644
--- a/app/models/work_items/widgets/hierarchy.rb
+++ b/app/models/work_items/widgets/hierarchy.rb
@@ -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
diff --git a/config/feature_flags/development/fetch_commits_for_bitbucket_server.yml b/config/feature_flags/development/fetch_commits_for_bitbucket_server.yml
new file mode 100644
index 00000000000..5524b0bf4d7
--- /dev/null
+++ b/config/feature_flags/development/fetch_commits_for_bitbucket_server.yml
@@ -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
diff --git a/config/metrics/counts_28d/20230927152527_i_quickactions_add_child_monthly.yml b/config/metrics/counts_28d/20230927152527_i_quickactions_add_child_monthly.yml
new file mode 100644
index 00000000000..bef39963b56
--- /dev/null
+++ b/config/metrics/counts_28d/20230927152527_i_quickactions_add_child_monthly.yml
@@ -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
diff --git a/config/metrics/counts_7d/20230922165305_i_quickactions_set_parent_monthly.yml b/config/metrics/counts_7d/20230922165305_i_quickactions_set_parent_monthly.yml
index 56dfb05114c..9af8f83ce79 100644
--- a/config/metrics/counts_7d/20230922165305_i_quickactions_set_parent_monthly.yml
+++ b/config/metrics/counts_7d/20230922165305_i_quickactions_set_parent_monthly.yml
@@ -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
diff --git a/config/metrics/counts_7d/20230927152525_i_quickactions_add_child_weekly.yml b/config/metrics/counts_7d/20230927152525_i_quickactions_add_child_weekly.yml
new file mode 100644
index 00000000000..7476a452d8f
--- /dev/null
+++ b/config/metrics/counts_7d/20230927152525_i_quickactions_add_child_weekly.yml
@@ -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
diff --git a/data/deprecations/16-4-deprecate-newly-detected-field.yml b/data/deprecations/16-4-deprecate-newly-detected-field.yml
new file mode 100644
index 00000000000..499bfd2147d
--- /dev/null
+++ b/data/deprecations/16-4-deprecate-newly-detected-field.yml
@@ -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
diff --git a/doc/update/deprecations.md b/doc/update/deprecations.md
index 4660b96bd53..3bb7f9816b4 100644
--- a/doc/update/deprecations.md
+++ b/doc/update/deprecations.md
@@ -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">
diff --git a/doc/user/analytics/value_streams_dashboard.md b/doc/user/analytics/value_streams_dashboard.md
index ed637dd886f..a0103663181 100644
--- a/doc/user/analytics/value_streams_dashboard.md
+++ b/doc/user/analytics/value_streams_dashboard.md
@@ -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.
diff --git a/doc/user/application_security/dependency_scanning/index.md b/doc/user/application_security/dependency_scanning/index.md
index f919f584c82..c04134de2b2 100644
--- a/doc/user/application_security/dependency_scanning/index.md
+++ b/doc/user/application_security/dependency_scanning/index.md
@@ -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>
diff --git a/doc/user/application_security/sast/rules.md b/doc/user/application_security/sast/rules.md
index 4e7a6387f9b..e4054764e1f 100644
--- a/doc/user/application_security/sast/rules.md
+++ b/doc/user/application_security/sast/rules.md
@@ -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.
diff --git a/doc/user/project/quick_actions.md b/doc/user/project/quick_actions.md
index 95f4f8b1d05..16967a3a46e 100644
--- a/doc/user/project/quick_actions.md
+++ b/doc/user/project/quick_actions.md
@@ -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`. |
diff --git a/lib/bitbucket_server/representation/pull_request.rb b/lib/bitbucket_server/representation/pull_request.rb
index 66dba5fefc7..996a10318f5 100644
--- a/lib/bitbucket_server/representation/pull_request.rb
+++ b/lib/bitbucket_server/representation/pull_request.rb
@@ -44,6 +44,10 @@ module BitbucketServer
state == 'merged'
end
+ def closed?
+ state == 'closed'
+ end
+
def created_at
self.class.convert_timestamp(created_date)
end
diff --git a/lib/gitlab/bitbucket_server_import/importers/pull_requests_importer.rb b/lib/gitlab/bitbucket_server_import/importers/pull_requests_importer.rb
index 92ec10bf037..ae73681f7f8 100644
--- a/lib/gitlab/bitbucket_server_import/importers/pull_requests_importer.rb
+++ b/lib/gitlab/bitbucket_server_import/importers/pull_requests_importer.rb
@@ -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
diff --git a/lib/gitlab/quick_actions/work_item_actions.rb b/lib/gitlab/quick_actions/work_item_actions.rb
index e302b832505..2adee0f9a9a 100644
--- a/lib/gitlab/quick_actions/work_item_actions.rb
+++ b/lib/gitlab/quick_actions/work_item_actions.rb
@@ -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
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 46033498331..822110ae218 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -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 ""
diff --git a/qa/qa/page/component/issue_board/show.rb b/qa/qa/page/component/issue_board/show.rb
index 41bb33ed943..6fbe2b7036c 100644
--- a/qa/qa/page/component/issue_board/show.rb
+++ b/qa/qa/page/component/issue_board/show.rb
@@ -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
diff --git a/qa/qa/specs/features/api/1_manage/import/import_large_github_repo_spec.rb b/qa/qa/specs/features/api/1_manage/import/import_large_github_repo_spec.rb
index 416162c806c..02b3d4cf32b 100644
--- a/qa/qa/specs/features/api/1_manage/import/import_large_github_repo_spec.rb
+++ b/qa/qa/specs/features/api/1_manage/import/import_large_github_repo_spec.rb
@@ -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
diff --git a/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_large_project_spec.rb b/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_large_project_spec.rb
index 7d7cef5b4fc..9032d07d1cd 100644
--- a/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_large_project_spec.rb
+++ b/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_large_project_spec.rb
@@ -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
diff --git a/scripts/internal_events/monitor.rb b/scripts/internal_events/monitor.rb
index 55811be999b..c7a261f62c3 100644
--- a/scripts/internal_events/monitor.rb
+++ b/scripts/internal_events/monitor.rb
@@ -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
diff --git a/spec/features/boards/sidebar_labels_in_namespaces_spec.rb b/spec/features/boards/sidebar_labels_in_namespaces_spec.rb
index ffed4a0854f..68c2b2587e7 100644
--- a/spec/features/boards/sidebar_labels_in_namespaces_spec.rb
+++ b/spec/features/boards/sidebar_labels_in_namespaces_spec.rb
@@ -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
diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb
index 358da1e1279..71cc9a28575 100644
--- a/spec/features/boards/sidebar_spec.rb
+++ b/spec/features/boards/sidebar_spec.rb
@@ -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
diff --git a/spec/features/boards/user_visits_board_spec.rb b/spec/features/boards/user_visits_board_spec.rb
index 5867ec17070..4741f58d883 100644
--- a/spec/features/boards/user_visits_board_spec.rb
+++ b/spec/features/boards/user_visits_board_spec.rb
@@ -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
diff --git a/spec/lib/bitbucket_server/representation/pull_request_spec.rb b/spec/lib/bitbucket_server/representation/pull_request_spec.rb
index 4d8bb3a4407..2d67dd88b24 100644
--- a/spec/lib/bitbucket_server/representation/pull_request_spec.rb
+++ b/spec/lib/bitbucket_server/representation/pull_request_spec.rb
@@ -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
diff --git a/spec/lib/gitlab/bitbucket_server_import/importers/pull_requests_importer_spec.rb b/spec/lib/gitlab/bitbucket_server_import/importers/pull_requests_importer_spec.rb
index b9a9c8dac29..af8a0202083 100644
--- a/spec/lib/gitlab/bitbucket_server_import/importers/pull_requests_importer_spec.rb
+++ b/spec/lib/gitlab/bitbucket_server_import/importers/pull_requests_importer_spec.rb
@@ -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
diff --git a/spec/services/notes/quick_actions_service_spec.rb b/spec/services/notes/quick_actions_service_spec.rb
index cb9d82535fa..0a16037c976 100644
--- a/spec/services/notes/quick_actions_service_spec.rb
+++ b/spec/services/notes/quick_actions_service_spec.rb
@@ -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) }
diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb
index 57967fa0c3a..2c34d6a59be 100644
--- a/spec/services/quick_actions/interpret_service_spec.rb
+++ b/spec/services/quick_actions/interpret_service_spec.rb
@@ -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