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 { - + +### Security policy field `newly_detected` is deprecated + +
+- Announced in GitLab 16.5 +- Removal in GitLab 17.0 ([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). +
+ +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`. + + + +
+ ### Self-managed certificate-based integration with Kubernetes
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 {
  • - Java 21 LTS is only available when using Maven and is not supported when - FIPS mode is enabled. + Java 21 LTS is only available when using Maven or Gradle. Java 21 LTS for sbt is not yet available and tracked in issue 421174. It is not supported when FIPS mode is enabled.

  • 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 ` | **{dotted-circle}** No | **{check-circle}** Yes | **{dotted-circle}** No | Add child to ``. The `` 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 ` | **{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