diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-11-17 15:10:53 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-11-17 15:10:53 +0300 |
commit | 2724004cd7b8fa9d179c926c62f2fd7be63c2d81 (patch) | |
tree | 9915474bdea07e9748e3b940de07e899fb5f641c | |
parent | f665874e9ee6c28d5098248852f07ae7469d8b2b (diff) |
Add latest changes from gitlab-org/gitlab@master
105 files changed, 948 insertions, 944 deletions
diff --git a/.gitlab/ci/package-and-test/main.gitlab-ci.yml b/.gitlab/ci/package-and-test/main.gitlab-ci.yml index 21dd8f957d4..1f7ed5aa2e6 100644 --- a/.gitlab/ci/package-and-test/main.gitlab-ci.yml +++ b/.gitlab/ci/package-and-test/main.gitlab-ci.yml @@ -76,8 +76,7 @@ instance-ff-inverse: - .parallel variables: QA_SCENARIO: Test::Instance::Image - QA_KNAPSACK_REPORT_NAME: ee-instance - GITLAB_QA_OPTS: --set-feature-flags $QA_FEATURE_FLAGS + QA_KNAPSACK_REPORT_NAME: instance rules: - !reference [.rules:test:feature-flags-set, rules] @@ -167,7 +166,7 @@ decomposition-single-db: - .parallel variables: QA_SCENARIO: Test::Instance::Image - GITLAB_QA_OPTS: --omnibus-config decomposition_single_db $EXTRA_GITLAB_QA_OPTS + GITLAB_QA_OPTS: --omnibus-config decomposition_single_db rules: - !reference [.rules:test:smoke-for-omnibus-mr, rules] - !reference [.rules:test:qa-parallel, rules] @@ -177,7 +176,7 @@ decomposition-single-db-selective: extends: .qa variables: QA_SCENARIO: Test::Instance::Image - GITLAB_QA_OPTS: --omnibus-config decomposition_single_db $EXTRA_GITLAB_QA_OPTS + GITLAB_QA_OPTS: --omnibus-config decomposition_single_db rules: - !reference [.rules:test:qa-selective, rules] - if: $QA_SUITES =~ /Test::Instance::All/ @@ -188,7 +187,7 @@ decomposition-single-db-selective-parallel: - .parallel variables: QA_SCENARIO: Test::Instance::Image - GITLAB_QA_OPTS: --omnibus-config decomposition_single_db $EXTRA_GITLAB_QA_OPTS + GITLAB_QA_OPTS: --omnibus-config decomposition_single_db rules: - !reference [.rules:test:qa-selective-parallel, rules] - if: $QA_SUITES =~ /Test::Instance::All/ @@ -204,7 +203,7 @@ decomposition-multiple-db: variables: QA_SCENARIO: Test::Instance::Image GITLAB_ALLOW_SEPARATE_CI_DATABASE: "true" - GITLAB_QA_OPTS: --omnibus-config decomposition_multiple_db $EXTRA_GITLAB_QA_OPTS + GITLAB_QA_OPTS: --omnibus-config decomposition_multiple_db rules: - !reference [.rules:test:smoke-for-omnibus-mr, rules] - !reference [.rules:test:qa-parallel, rules] @@ -215,7 +214,7 @@ decomposition-multiple-db-selective: variables: QA_SCENARIO: Test::Instance::Image GITLAB_ALLOW_SEPARATE_CI_DATABASE: "true" - GITLAB_QA_OPTS: --omnibus-config decomposition_multiple_db $EXTRA_GITLAB_QA_OPTS + GITLAB_QA_OPTS: --omnibus-config decomposition_multiple_db rules: - !reference [.rules:test:qa-selective, rules] - if: $QA_SUITES =~ /Test::Instance::All/ @@ -227,7 +226,7 @@ decomposition-multiple-db-selective-parallel: variables: QA_SCENARIO: Test::Instance::Image GITLAB_ALLOW_SEPARATE_CI_DATABASE: "true" - GITLAB_QA_OPTS: --omnibus-config decomposition_multiple_db $EXTRA_GITLAB_QA_OPTS + GITLAB_QA_OPTS: --omnibus-config decomposition_multiple_db rules: - !reference [.rules:test:qa-selective-parallel, rules] - if: $QA_SUITES =~ /Test::Instance::All/ @@ -244,7 +243,7 @@ object-storage: variables: QA_SCENARIO: Test::Instance::Image QA_RSPEC_TAGS: --tag object_storage - GITLAB_QA_OPTS: --omnibus-config object_storage $EXTRA_GITLAB_QA_OPTS + GITLAB_QA_OPTS: --omnibus-config object_storage rules: - !reference [.rules:test:qa-parallel, rules] - if: $QA_SUITES =~ /Test::Instance::ObjectStorage/ @@ -254,7 +253,7 @@ object-storage-selective: variables: QA_SCENARIO: Test::Instance::Image QA_RSPEC_TAGS: --tag object_storage - GITLAB_QA_OPTS: --omnibus-config object_storage $EXTRA_GITLAB_QA_OPTS + GITLAB_QA_OPTS: --omnibus-config object_storage rules: - !reference [.rules:test:qa-selective, rules] - if: $QA_SUITES =~ /Test::Instance::ObjectStorage/ @@ -265,7 +264,7 @@ object-storage-selective-parallel: variables: QA_SCENARIO: Test::Instance::Image QA_RSPEC_TAGS: --tag object_storage - GITLAB_QA_OPTS: --omnibus-config object_storage $EXTRA_GITLAB_QA_OPTS + GITLAB_QA_OPTS: --omnibus-config object_storage rules: - !reference [.rules:test:qa-selective-parallel, rules] - if: $QA_SUITES =~ /Test::Instance::ObjectStorage/ @@ -283,7 +282,7 @@ object-storage-aws: AWS_S3_BUCKET_NAME: $QA_AWS_S3_BUCKET_NAME AWS_S3_KEY_ID: $QA_AWS_S3_KEY_ID AWS_S3_REGION: $QA_AWS_S3_REGION - GITLAB_QA_OPTS: --omnibus-config object_storage_aws $EXTRA_GITLAB_QA_OPTS + GITLAB_QA_OPTS: --omnibus-config object_storage_aws object-storage-aws-selective: extends: object-storage-selective @@ -292,7 +291,7 @@ object-storage-aws-selective: AWS_S3_BUCKET_NAME: $QA_AWS_S3_BUCKET_NAME AWS_S3_KEY_ID: $QA_AWS_S3_KEY_ID AWS_S3_REGION: $QA_AWS_S3_REGION - GITLAB_QA_OPTS: --omnibus-config object_storage_aws $EXTRA_GITLAB_QA_OPTS + GITLAB_QA_OPTS: --omnibus-config object_storage_aws object-storage-aws-selective-parallel: extends: object-storage-selective-parallel @@ -301,7 +300,7 @@ object-storage-aws-selective-parallel: AWS_S3_BUCKET_NAME: $QA_AWS_S3_BUCKET_NAME AWS_S3_KEY_ID: $QA_AWS_S3_KEY_ID AWS_S3_REGION: $QA_AWS_S3_REGION - GITLAB_QA_OPTS: --omnibus-config object_storage_aws $EXTRA_GITLAB_QA_OPTS + GITLAB_QA_OPTS: --omnibus-config object_storage_aws # ========== object-storage-gcs =========== @@ -315,7 +314,7 @@ object-storage-gcs: GOOGLE_PROJECT: $QA_GOOGLE_PROJECT GOOGLE_JSON_KEY: $QA_GOOGLE_JSON_KEY GOOGLE_CLIENT_EMAIL: $QA_GOOGLE_CLIENT_EMAIL - GITLAB_QA_OPTS: --omnibus-config object_storage_gcs $EXTRA_GITLAB_QA_OPTS + GITLAB_QA_OPTS: --omnibus-config object_storage_gcs object-storage-gcs-selective: extends: object-storage-selective @@ -324,7 +323,7 @@ object-storage-gcs-selective: GOOGLE_PROJECT: $QA_GOOGLE_PROJECT GOOGLE_JSON_KEY: $QA_GOOGLE_JSON_KEY GOOGLE_CLIENT_EMAIL: $QA_GOOGLE_CLIENT_EMAIL - GITLAB_QA_OPTS: --omnibus-config object_storage_gcs $EXTRA_GITLAB_QA_OPTS + GITLAB_QA_OPTS: --omnibus-config object_storage_gcs object-storage-gcs-selective-parallel: extends: object-storage-selective-parallel @@ -333,7 +332,7 @@ object-storage-gcs-selective-parallel: GOOGLE_PROJECT: $QA_GOOGLE_PROJECT GOOGLE_JSON_KEY: $QA_GOOGLE_JSON_KEY GOOGLE_CLIENT_EMAIL: $QA_GOOGLE_CLIENT_EMAIL - GITLAB_QA_OPTS: --omnibus-config object_storage_gcs $EXTRA_GITLAB_QA_OPTS + GITLAB_QA_OPTS: --omnibus-config object_storage_gcs # ------------------------------------------ # Non parallel jobs @@ -567,7 +566,7 @@ registry-object-storage-tls: QA_SCENARIO: Test::Integration::RegistryTLS QA_RSPEC_TAGS: "" GITLAB_TLS_CERTIFICATE: $QA_GITLAB_TLS_CERTIFICATE - GITLAB_QA_OPTS: --omnibus-config registry_object_storage $EXTRA_GITLAB_QA_OPTS + GITLAB_QA_OPTS: --omnibus-config registry_object_storage importers: extends: diff --git a/.gitlab/ci/qa-common/main.gitlab-ci.yml b/.gitlab/ci/qa-common/main.gitlab-ci.yml index 94236730f6a..c44aa327762 100644 --- a/.gitlab/ci/qa-common/main.gitlab-ci.yml +++ b/.gitlab/ci/qa-common/main.gitlab-ci.yml @@ -68,7 +68,6 @@ stages: QA_INTERCEPT_REQUESTS: "true" GITLAB_LICENSE_MODE: test GITLAB_QA_ADMIN_ACCESS_TOKEN: $QA_ADMIN_ACCESS_TOKEN - GITLAB_QA_OPTS: $EXTRA_GITLAB_QA_OPTS before_script: - !reference [.qa-base, before_script] # Prepend the file paths with the absolute path from inside the container since the files will be read from there diff --git a/.rubocop_todo/rspec/feature_category.yml b/.rubocop_todo/rspec/feature_category.yml index eb740c099c3..1aee8934702 100644 --- a/.rubocop_todo/rspec/feature_category.yml +++ b/.rubocop_todo/rspec/feature_category.yml @@ -4797,7 +4797,6 @@ RSpec/FeatureCategory: - 'spec/models/time_tracking/timelog_category_spec.rb' - 'spec/models/todo_spec.rb' - 'spec/models/token_with_iv_spec.rb' - - 'spec/models/tree_spec.rb' - 'spec/models/trending_project_spec.rb' - 'spec/models/upload_spec.rb' - 'spec/models/uploads/fog_spec.rb' diff --git a/.rubocop_todo/rspec/named_subject.yml b/.rubocop_todo/rspec/named_subject.yml index c99d6784b73..5099162c5a8 100644 --- a/.rubocop_todo/rspec/named_subject.yml +++ b/.rubocop_todo/rspec/named_subject.yml @@ -2356,7 +2356,6 @@ RSpec/NamedSubject: - 'spec/lib/gitlab/pagination/offset_pagination_spec.rb' - 'spec/lib/gitlab/patch/draw_route_spec.rb' - 'spec/lib/gitlab/patch/prependable_spec.rb' - - 'spec/lib/gitlab/patch/sidekiq_scheduled_enq_spec.rb' - 'spec/lib/gitlab/path_regex_spec.rb' - 'spec/lib/gitlab/performance_bar/stats_spec.rb' - 'spec/lib/gitlab/performance_bar/with_top_level_warnings_spec.rb' diff --git a/.rubocop_todo/style/inline_disable_annotation.yml b/.rubocop_todo/style/inline_disable_annotation.yml index 9fcd52993bc..faa79f6aa3a 100644 --- a/.rubocop_todo/style/inline_disable_annotation.yml +++ b/.rubocop_todo/style/inline_disable_annotation.yml @@ -2677,7 +2677,6 @@ Style/InlineDisableAnnotation: - 'lib/gitlab/patch/prependable.rb' - 'lib/gitlab/patch/redis_cache_store.rb' - 'lib/gitlab/patch/sidekiq_cron_poller.rb' - - 'lib/gitlab/patch/sidekiq_scheduled_enq.rb' - 'lib/gitlab/performance_bar.rb' - 'lib/gitlab/performance_bar/redis_adapter_when_peek_enabled.rb' - 'lib/gitlab/popen/runner.rb' diff --git a/app/assets/javascripts/ci/catalog/components/list/ci_resources_list_item.vue b/app/assets/javascripts/ci/catalog/components/list/ci_resources_list_item.vue index 3544ff448b7..9540e1ed3ea 100644 --- a/app/assets/javascripts/ci/catalog/components/list/ci_resources_list_item.vue +++ b/app/assets/javascripts/ci/catalog/components/list/ci_resources_list_item.vue @@ -112,7 +112,7 @@ export default { </div> </div> <div - class="gl-display-flex gl-sm-flex-direction-column gl-justify-content-space-between gl-font-sm" + class="gl-display-flex gl-flex-direction-column gl-md-flex-direction-row gl-justify-content-space-between gl-font-sm" > <span class="gl-display-flex gl-flex-basis-two-thirds">{{ resource.description }}</span> <div class="gl-display-flex gl-justify-content-end"> diff --git a/app/assets/javascripts/commons/index.js b/app/assets/javascripts/commons/index.js index d2a5ef83faf..6c9283a22cf 100644 --- a/app/assets/javascripts/commons/index.js +++ b/app/assets/javascripts/commons/index.js @@ -3,6 +3,3 @@ import './bootstrap'; import './vue'; import './gitlab_ui'; import '../lib/utils/axios_utils'; -import { openUserCountsBroadcast } from './nav/user_merge_requests'; - -openUserCountsBroadcast(); diff --git a/app/assets/javascripts/commons/nav/user_merge_requests.js b/app/assets/javascripts/commons/nav/user_merge_requests.js deleted file mode 100644 index 90dca0310f3..00000000000 --- a/app/assets/javascripts/commons/nav/user_merge_requests.js +++ /dev/null @@ -1,93 +0,0 @@ -import { getUserCounts } from '~/rest_api'; - -let channel; - -function broadcastCount(newCount) { - if (!channel) { - return; - } - - channel.postMessage(newCount); -} - -function updateUserMergeRequestCounts(newCount) { - const mergeRequestsCountEl = document.querySelector('.js-assigned-mr-count'); - mergeRequestsCountEl.textContent = newCount.toLocaleString(); -} - -function updateReviewerMergeRequestCounts(newCount) { - const mergeRequestsCountEl = document.querySelector('.js-reviewer-mr-count'); - mergeRequestsCountEl.textContent = newCount.toLocaleString(); -} - -function updateMergeRequestCounts(newCount) { - const mergeRequestsCountEl = document.querySelector('.js-merge-requests-count'); - mergeRequestsCountEl.textContent = newCount.toLocaleString(); - mergeRequestsCountEl.classList.toggle('gl-display-none', Number(newCount) === 0); -} - -/** - * Refresh user counts (and broadcast if open) - */ -export function refreshUserMergeRequestCounts() { - if (gon?.use_new_navigation) { - // The new sidebar manages _all_ the counts in - // ~/super_sidebar/user_counts_manager.js - document.dispatchEvent(new CustomEvent('userCounts:fetch')); - return Promise.resolve(); - } - return getUserCounts() - .then(({ data }) => { - const assignedMergeRequests = data.assigned_merge_requests; - const reviewerMergeRequests = data.review_requested_merge_requests; - const fullCount = assignedMergeRequests + reviewerMergeRequests; - - updateUserMergeRequestCounts(assignedMergeRequests); - updateReviewerMergeRequestCounts(reviewerMergeRequests); - updateMergeRequestCounts(fullCount); - broadcastCount(fullCount); - }) - .catch((ex) => { - console.error(ex); // eslint-disable-line no-console - }); -} - -/** - * Close the broadcast channel for user counts - */ -export function closeUserCountsBroadcast() { - if (!channel) { - return; - } - - channel.close(); - channel = null; -} - -/** - * Open the broadcast channel for user counts, adds user id so we only update - * - * **Please note:** - * Not supported in all browsers, but not polyfilling for now - * to keep bundle size small and - * no special functionality lost except cross tab notifications - */ -export function openUserCountsBroadcast() { - if (gon?.use_new_navigation) { - // The new sidebar broadcasts _all counts_ and updates - // them accordingly. Therefore we do not need this manager - // ~/super_sidebar/user_counts_manager.js - return; - } - closeUserCountsBroadcast(); - - if (window.BroadcastChannel) { - const currentUserId = typeof gon !== 'undefined' && gon && gon.current_user_id; - if (currentUserId) { - channel = new BroadcastChannel(`mr_count_channel_${currentUserId}`); - channel.onmessage = (ev) => { - updateMergeRequestCounts(ev.data); - }; - } - } -} diff --git a/app/assets/javascripts/graphql_shared/possible_types.json b/app/assets/javascripts/graphql_shared/possible_types.json index 1439a3181b0..ee5f8c2ac2f 100644 --- a/app/assets/javascripts/graphql_shared/possible_types.json +++ b/app/assets/javascripts/graphql_shared/possible_types.json @@ -201,5 +201,9 @@ "WorkItemWidgetStatus", "WorkItemWidgetTestReports", "WorkItemWidgetWeight" + ], + "WorkItemWidgetDefinition": [ + "WorkItemWidgetDefinitionAssignees", + "WorkItemWidgetDefinitionGeneric" ] } diff --git a/app/assets/javascripts/invite_members/components/invite_group_trigger.vue b/app/assets/javascripts/invite_members/components/invite_group_trigger.vue index 424a9d3fabd..b0cfe670edc 100644 --- a/app/assets/javascripts/invite_members/components/invite_group_trigger.vue +++ b/app/assets/javascripts/invite_members/components/invite_group_trigger.vue @@ -28,12 +28,7 @@ export default { </script> <template> - <gl-button - :class="classes" - data-qa-selector="invite_a_group_button" - data-test-id="invite-group-button" - @click="openModal" - > + <gl-button :class="classes" data-testid="invite-group-button" @click="openModal"> {{ displayText }} </gl-button> </template> diff --git a/app/assets/javascripts/invite_members/components/invite_members_trigger.vue b/app/assets/javascripts/invite_members/components/invite_members_trigger.vue index 6efb7a6cdf1..b37333d923c 100644 --- a/app/assets/javascripts/invite_members/components/invite_members_trigger.vue +++ b/app/assets/javascripts/invite_members/components/invite_members_trigger.vue @@ -4,7 +4,7 @@ import { s__ } from '~/locale'; import eventHub from '../event_hub'; import { TRIGGER_ELEMENT_BUTTON, - TRIGGER_DEFAULT_QA_SELECTOR, + TRIGGER_DEFAULT_TESTID, TRIGGER_ELEMENT_WITH_EMOJI, TRIGGER_ELEMENT_DROPDOWN_WITH_EMOJI, TRIGGER_ELEMENT_DISCLOSURE_DROPDOWN, @@ -42,18 +42,17 @@ export default { required: false, default: 'button', }, - qaSelector: { + testId: { type: String, required: false, - default: TRIGGER_DEFAULT_QA_SELECTOR, + default: TRIGGER_DEFAULT_TESTID, }, }, computed: { componentAttributes() { return { class: this.classes, - 'data-qa-selector': this.qaSelector, - 'data-test-id': 'invite-members-button', + 'data-testid': this.testId, }; }, item() { diff --git a/app/assets/javascripts/invite_members/components/invite_modal_base.vue b/app/assets/javascripts/invite_members/components/invite_modal_base.vue index 81aea12f311..86e3e56a525 100644 --- a/app/assets/javascripts/invite_members/components/invite_modal_base.vue +++ b/app/assets/javascripts/invite_members/components/invite_modal_base.vue @@ -168,7 +168,6 @@ export default { variant: 'confirm', disabled: this.submitDisabled, loading: this.isLoading, - 'data-qa-selector': 'invite_button', }, }; }, @@ -298,7 +297,6 @@ export default { <gl-collapsible-listbox :id="dropdownId" v-model="selectedAccessLevel" - data-qa-selector="access_level_dropdown" data-testid="access-level-dropdown" :items="accessLevelsOptions" block diff --git a/app/assets/javascripts/invite_members/components/members_token_select.vue b/app/assets/javascripts/invite_members/components/members_token_select.vue index 0be04b7af35..015cadc9993 100644 --- a/app/assets/javascripts/invite_members/components/members_token_select.vue +++ b/app/assets/javascripts/invite_members/components/members_token_select.vue @@ -102,7 +102,6 @@ export default { textInputAttrs() { return { 'data-testid': 'members-token-select-input', - 'data-qa-selector': 'members_token_select_input', id: this.inputId, }; }, diff --git a/app/assets/javascripts/invite_members/constants.js b/app/assets/javascripts/invite_members/constants.js index 93386e5504b..f44313ff41f 100644 --- a/app/assets/javascripts/invite_members/constants.js +++ b/app/assets/javascripts/invite_members/constants.js @@ -20,7 +20,7 @@ export const TRIGGER_ELEMENT_WITH_EMOJI = 'text-emoji'; export const TRIGGER_ELEMENT_DROPDOWN_WITH_EMOJI = 'dropdown-text-emoji'; export const TRIGGER_ELEMENT_DISCLOSURE_DROPDOWN = 'dropdown-text'; export const INVITE_MEMBER_MODAL_TRACKING_CATEGORY = 'invite_members_modal'; -export const TRIGGER_DEFAULT_QA_SELECTOR = 'invite_members_button'; +export const TRIGGER_DEFAULT_TESTID = 'invite-members-button'; export const IMPORT_PROJECT_MEMBERS_MODAL_TRACKING_CATEGORY = 'invite_project_members_modal'; export const IMPORT_PROJECT_MEMBERS_MODAL_TRACKING_LABEL = 'project-members-page'; export const MEMBERS_MODAL_DEFAULT_TITLE = s__('InviteMembersModal|Invite members'); diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue index 329d6cfec00..87b55b19c08 100644 --- a/app/assets/javascripts/notes/components/comment_form.vue +++ b/app/assets/javascripts/notes/components/comment_form.vue @@ -3,7 +3,6 @@ import { GlAlert, GlButton, GlIcon, GlFormCheckbox, GlTooltipDirective } from '@ import $ from 'jquery'; // eslint-disable-next-line no-restricted-imports import { mapActions, mapGetters, mapState } from 'vuex'; -import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests'; import { createAlert } from '~/alert'; import { STATUS_CLOSED, STATUS_MERGED, STATUS_OPEN, STATUS_REOPENED } from '~/issues/constants'; import { containsSensitiveToken, confirmSensitiveAction } from '~/lib/utils/secret_detection'; @@ -17,6 +16,7 @@ import { badgeState } from '~/merge_requests/components/merge_request_header.vue import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue'; import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue'; import { trackSavedUsingEditor } from '~/vue_shared/components/markdown/tracking'; +import { fetchUserCounts } from '~/super_sidebar/user_counts_fetch'; import * as constants from '../constants'; import eventHub from '../event_hub'; @@ -297,8 +297,10 @@ export default { const toggleState = this.isOpen ? this.closeIssuable : this.reopenIssuable; toggleState() - .then(() => badgeState.updateStatus && badgeState.updateStatus()) - .then(refreshUserMergeRequestCounts) + .then(() => { + fetchUserCounts(); + return badgeState?.updateStatus(); + }) .catch(() => createAlert({ message: constants.toggleStateErrorMessage[this.noteableType][this.openState], diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue index 0563ed8394c..897cd3583c8 100644 --- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue +++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue @@ -1,5 +1,4 @@ <script> -import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests'; import { createAlert } from '~/alert'; import { TYPE_ISSUE } from '~/issues/constants'; import { __ } from '~/locale'; @@ -104,8 +103,6 @@ export default { .then(() => { this.loading = false; this.store.resetChanging(); - - refreshUserMergeRequestCounts(); }) .catch(() => { this.loading = false; diff --git a/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue b/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue index 440d0344ae0..1d1dbd51756 100644 --- a/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue +++ b/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue @@ -2,12 +2,12 @@ // NOTE! For the first iteration, we are simply copying the implementation of Assignees // It will soon be overhauled in Issue https://gitlab.com/gitlab-org/gitlab/-/issues/233736 import Vue from 'vue'; -import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests'; import { createAlert } from '~/alert'; import { TYPE_ISSUE } from '~/issues/constants'; import { __ } from '~/locale'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; +import { fetchUserCounts } from '~/super_sidebar/user_counts_fetch'; import eventHub from '../../event_hub'; import getMergeRequestReviewersQuery from '../../queries/get_merge_request_reviewers.query.graphql'; import mergeRequestReviewersUpdatedSubscription from '../../queries/merge_request_reviewers.subscription.graphql'; @@ -157,7 +157,7 @@ export default { .saveReviewers(this.field) .then(() => { this.loading = false; - refreshUserMergeRequestCounts(); + fetchUserCounts(); this.$apollo.queries.issuable.refetch(); }) .catch(() => { diff --git a/app/assets/javascripts/super_sidebar/user_counts_fetch.js b/app/assets/javascripts/super_sidebar/user_counts_fetch.js new file mode 100644 index 00000000000..779cb2609c2 --- /dev/null +++ b/app/assets/javascripts/super_sidebar/user_counts_fetch.js @@ -0,0 +1,10 @@ +/** + * This triggers a re-fetch of the user counts + * + * It is separate from the user_counts_manager, so that + * this function is side-effect free and can be used in + * anywhere in the app without bloating bundle size + */ +export function fetchUserCounts() { + document.dispatchEvent(new CustomEvent('userCounts:fetch')); +} diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.vue index 9258bc39bcb..2c5f6b9a3ec 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.vue @@ -1,9 +1,9 @@ <script> -import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests'; import { STATUS_MERGED } from '~/issues/constants'; import simplePoll from '~/lib/utils/simple_poll'; import MergeRequest from '~/merge_request'; import BoldText from '~/vue_merge_request_widget/components/bold_text.vue'; +import { fetchUserCounts } from '~/super_sidebar/user_counts_fetch'; import eventHub from '../../event_hub'; import { MERGE_ACTIVE_STATUS_PHRASES, STATE_MACHINE } from '../../constants'; import StatusIcon from '../mr_widget_status_icon.vue'; @@ -58,7 +58,7 @@ export default { MergeRequest.decreaseCounter(); stopPolling(); - refreshUserMergeRequestCounts(); + fetchUserCounts(); // If user checked remove source branch and we didn't remove the branch yet // we should start another polling for source branch remove process diff --git a/app/assets/javascripts/work_items/components/work_item_parent.vue b/app/assets/javascripts/work_items/components/work_item_parent.vue index ce30f7985cf..0c0842a3e05 100644 --- a/app/assets/javascripts/work_items/components/work_item_parent.vue +++ b/app/assets/javascripts/work_items/components/work_item_parent.vue @@ -211,6 +211,7 @@ export default { id="work-item-parent-listbox-value" class="gl-max-w-max-content" data-testid="work-item-parent-listbox" + block searchable is-check-centered category="tertiary" diff --git a/app/assets/stylesheets/application_utilities.scss b/app/assets/stylesheets/application_utilities.scss index 8bec12784ed..817e983a0ec 100644 --- a/app/assets/stylesheets/application_utilities.scss +++ b/app/assets/stylesheets/application_utilities.scss @@ -10,5 +10,3 @@ // Gitlab UI util classes @import '@gitlab/ui/src/scss/utilities'; - -@import 'tmp_utilities';
\ No newline at end of file diff --git a/app/assets/stylesheets/tmp_utilities.scss b/app/assets/stylesheets/tmp_utilities.scss deleted file mode 100644 index c8144e97693..00000000000 --- a/app/assets/stylesheets/tmp_utilities.scss +++ /dev/null @@ -1,44 +0,0 @@ -/** - * DISCLAIMER - * This is a temporary stylesheet meant to assist in migrating away from desktop-first responsive - * CSS utilities. - * DO NOT add utils in here unless you are actively taking part in in the migration. - * We needed this new file for temporary utils to be defined _after_ the main, non-responsive - * GitLab UI util. - * This file is scheduled to be removed by the end of 2023. - */ - .gl-sm-w-25p { - @include gl-media-breakpoint-up(sm) { - width: 25%; - } -} - -.gl-sm-w-30p { - @include gl-media-breakpoint-up(sm) { - width: 30%; - } -} - -.gl-sm-w-40p { - @include gl-media-breakpoint-up(sm) { - width: 40%; - } -} - -.gl-sm-w-75p { - @include gl-media-breakpoint-up(sm) { - width: 75%; - } -} - -.gl-sm-flex-direction-row-reverse { - @include gl-media-breakpoint-up(sm) { - flex-direction: row-reverse; - } -} - -.gl-sm-mb-0 { - @include gl-media-breakpoint-up(sm) { - margin-bottom: 0; - } -} diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index ce0c8291eda..6368ae914dc 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -344,15 +344,9 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo end def discussions - if Feature.enabled?(:only_highlight_discussions_requested, project) - super do |discussion_notes| - note_ids = discussion_notes.flat_map { |x| x.notes.collect(&:id) } - merge_request.discussions_diffs.load_highlight(diff_note_ids: note_ids) - end - else - merge_request.discussions_diffs.load_highlight - - super + super do |discussion_notes| + note_ids = discussion_notes.flat_map { |x| x.notes.collect(&:id) } + merge_request.discussions_diffs.load_highlight(diff_note_ids: note_ids) end end diff --git a/app/graphql/resolvers/work_items/types_resolver.rb b/app/graphql/resolvers/work_items/types_resolver.rb index 2508125d392..d560c2bf612 100644 --- a/app/graphql/resolvers/work_items/types_resolver.rb +++ b/app/graphql/resolvers/work_items/types_resolver.rb @@ -3,6 +3,8 @@ module Resolvers module WorkItems class TypesResolver < BaseResolver + include LooksAhead + type Types::WorkItems::TypeType.connection_type, null: true argument :taskable, ::GraphQL::Types::Boolean, @@ -10,13 +12,23 @@ module Resolvers description: 'If `true`, only taskable work item types will be returned.' \ ' Argument is experimental and can be removed in the future without notice.' - def resolve(taskable: nil) + def resolve_with_lookahead(taskable: nil) + context.scoped_set!(:resource_parent, object) + # This will require a finder in the future when groups/projects get their work item types # All groups/projects use the default types for now base_scope = ::WorkItems::Type.default base_scope = base_scope.by_type(:task) if taskable - base_scope.order_by_name_asc + apply_lookahead(base_scope.order_by_name_asc) + end + + private + + def preloads + { + widget_definitions: :enabled_widget_definitions + } end end end diff --git a/app/graphql/types/work_item_type.rb b/app/graphql/types/work_item_type.rb index 103a1c0ec9b..b42684e650b 100644 --- a/app/graphql/types/work_item_type.rb +++ b/app/graphql/types/work_item_type.rb @@ -67,6 +67,12 @@ module Types expose_permissions Types::PermissionTypes::WorkItem + def work_item_type + context.scoped_set!(:resource_parent, object.resource_parent) + + object.work_item_type + end + def web_url Gitlab::UrlBuilder.build(object) end diff --git a/app/graphql/types/work_items/type_type.rb b/app/graphql/types/work_items/type_type.rb index 4d008a21b9c..b42d73544dc 100644 --- a/app/graphql/types/work_items/type_type.rb +++ b/app/graphql/types/work_items/type_type.rb @@ -7,12 +7,20 @@ module Types authorize :read_work_item_type - field :icon_name, GraphQL::Types::String, null: true, - description: 'Icon name of the work item type.' - field :id, Types::GlobalIDType[::WorkItems::Type], null: false, - description: 'Global ID of the work item type.' - field :name, GraphQL::Types::String, null: false, - description: 'Name of the work item type.' + field :icon_name, GraphQL::Types::String, + null: true, + description: 'Icon name of the work item type.' + field :id, Types::GlobalIDType[::WorkItems::Type], + null: false, + description: 'Global ID of the work item type.' + field :name, GraphQL::Types::String, + null: false, + description: 'Name of the work item type.' + field :widget_definitions, [Types::WorkItems::WidgetDefinitionInterface], + null: true, + description: 'Available widgets for the work item type.', + method: :widgets, + alpha: { milestone: '16.7' } end end end diff --git a/app/graphql/types/work_items/widget_definition_interface.rb b/app/graphql/types/work_items/widget_definition_interface.rb new file mode 100644 index 00000000000..0cfa6c978fc --- /dev/null +++ b/app/graphql/types/work_items/widget_definition_interface.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Types + module WorkItems + module WidgetDefinitionInterface + include Types::BaseInterface + + graphql_name 'WorkItemWidgetDefinition' + + field :type, ::Types::WorkItems::WidgetTypeEnum, + null: false, + description: 'Widget type.' + + ORPHAN_TYPES = [ + ::Types::WorkItems::WidgetDefinitions::AssigneesType, + ::Types::WorkItems::WidgetDefinitions::GenericType + ].freeze + + def self.resolve_type(object, _context) + if object == ::WorkItems::Widgets::Assignees + ::Types::WorkItems::WidgetDefinitions::AssigneesType + else + ::Types::WorkItems::WidgetDefinitions::GenericType + end + end + + orphan_types(*ORPHAN_TYPES) + end + end +end diff --git a/app/graphql/types/work_items/widget_definitions/assignees_type.rb b/app/graphql/types/work_items/widget_definitions/assignees_type.rb new file mode 100644 index 00000000000..3113543ec1b --- /dev/null +++ b/app/graphql/types/work_items/widget_definitions/assignees_type.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Types + module WorkItems + module WidgetDefinitions + # rubocop:disable Graphql/AuthorizeTypes -- Authorization too granular, parent type is authorized + class AssigneesType < BaseObject + graphql_name 'WorkItemWidgetDefinitionAssignees' + description 'Represents an assignees widget definition' + + implements Types::WorkItems::WidgetDefinitionInterface + + field :can_invite_members, GraphQL::Types::Boolean, + null: false, + description: 'Indicates whether the current user can invite members to the work item\'s parent.' + + def can_invite_members + object.can_invite_members?(current_user, resource_parent) + end + + private + + def resource_parent + context[:resource_parent] + end + end + # rubocop:enable Graphql/AuthorizeTypes + end + end +end diff --git a/app/graphql/types/work_items/widget_definitions/generic_type.rb b/app/graphql/types/work_items/widget_definitions/generic_type.rb new file mode 100644 index 00000000000..f3817ade654 --- /dev/null +++ b/app/graphql/types/work_items/widget_definitions/generic_type.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Types + module WorkItems + module WidgetDefinitions + # rubocop:disable Graphql/AuthorizeTypes -- Authorization too granular, parent type is authorized + class GenericType < BaseObject + graphql_name 'WorkItemWidgetDefinitionGeneric' + description 'Represents a generic widget definition' + + implements Types::WorkItems::WidgetDefinitionInterface + end + # rubocop:enable Graphql/AuthorizeTypes + end + end +end diff --git a/app/models/ability.rb b/app/models/ability.rb index b8433191d84..f1db4be8eb4 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -78,6 +78,15 @@ class Ability policy = policy_for(user, subject) + # https://gitlab.com/gitlab-org/gitlab/-/issues/421150#note_1638311666 + if ability == :read_namespace && Feature.enabled?(:log_read_namespace_usages, Feature.current_request) + Gitlab::AppLogger.info( + message: 'Ability is in use', + ability: ability, + caller_locations: caller_locations(1, 5).map(&:to_s) + ) + end + before_check(policy, ability.to_sym, user, subject, opts) case opts[:scope] diff --git a/app/models/bulk_import.rb b/app/models/bulk_import.rb index a7ace7429d7..80e0412ad19 100644 --- a/app/models/bulk_import.rb +++ b/app/models/bulk_import.rb @@ -16,7 +16,7 @@ class BulkImport < ApplicationRecord enum source_type: { gitlab: 0 } - scope :stale, -> { where('created_at < ?', 8.hours.ago).where(status: [0, 1]) } + scope :stale, -> { where('created_at < ?', 24.hours.ago).where(status: [0, 1]) } scope :order_by_created_at, ->(direction) { order(created_at: direction) } state_machine :status, initial: :created do diff --git a/app/models/bulk_imports/entity.rb b/app/models/bulk_imports/entity.rb index a075c2f7e4f..d7ccd33d19d 100644 --- a/app/models/bulk_imports/entity.rb +++ b/app/models/bulk_imports/entity.rb @@ -54,7 +54,7 @@ class BulkImports::Entity < ApplicationRecord enum source_type: { group_entity: 0, project_entity: 1 } scope :by_user_id, ->(user_id) { joins(:bulk_import).where(bulk_imports: { user_id: user_id }) } - scope :stale, -> { where('created_at < ?', 8.hours.ago).where(status: [0, 1]) } + scope :stale, -> { where('created_at < ?', 24.hours.ago).where(status: [0, 1]) } scope :by_bulk_import_id, ->(bulk_import_id) { where(bulk_import_id: bulk_import_id) } scope :order_by_created_at, ->(direction) { order(created_at: direction) } diff --git a/app/models/bulk_imports/tracker.rb b/app/models/bulk_imports/tracker.rb index d9efd489af5..b06583c8e06 100644 --- a/app/models/bulk_imports/tracker.rb +++ b/app/models/bulk_imports/tracker.rb @@ -21,7 +21,7 @@ class BulkImports::Tracker < ApplicationRecord validates :stage, presence: true - delegate :file_extraction_pipeline?, to: :pipeline_class + delegate :file_extraction_pipeline?, :abort_on_failure?, to: :pipeline_class DEFAULT_PAGE_SIZE = 500 STALE_AFTER = 4.hours diff --git a/app/models/deployment.rb b/app/models/deployment.rb index f0093445ba8..36f4a0ef426 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -395,9 +395,9 @@ class Deployment < ApplicationRecord def update_status(status) update_status!(status) rescue StandardError => e - Gitlab::ErrorTracking.track_exception( - StatusUpdateError.new(e.message), deployment_id: self.id) - + error = StatusUpdateError.new(e.message) + error.set_backtrace(caller) + Gitlab::ErrorTracking.track_exception(error, deployment_id: self.id) false end @@ -410,9 +410,9 @@ class Deployment < ApplicationRecord update_status!(job_status) rescue StandardError => e - Gitlab::ErrorTracking.track_exception( - StatusSyncError.new(e.message), deployment_id: self.id, job_id: job.id) - + error = StatusSyncError.new(e.message) + error.set_backtrace(caller) + Gitlab::ErrorTracking.track_exception(error, deployment_id: self.id, job_id: job.id) false end diff --git a/app/models/project.rb b/app/models/project.rb index 37e86df9b7c..7ff32706566 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -506,7 +506,7 @@ class Project < ApplicationRecord delegate :merge_requests_access_level, :forking_access_level, :issues_access_level, :wiki_access_level, :snippets_access_level, :builds_access_level, :repository_access_level, :package_registry_access_level, :pages_access_level, :metrics_dashboard_access_level, :analytics_access_level, :operations_access_level, :security_and_compliance_access_level, :container_registry_access_level, :environments_access_level, :feature_flags_access_level, :monitor_access_level, :releases_access_level, :infrastructure_access_level, :model_experiments_access_level, to: :project_feature, allow_nil: true delegate :name, to: :owner, allow_nil: true, prefix: true - delegate :jira_dvcs_server_last_sync_at, :jira_dvcs_cloud_last_sync_at, to: :feature_usage + delegate :jira_dvcs_server_last_sync_at, to: :feature_usage delegate :last_pipeline, to: :commit, allow_nil: true with_options to: :team do @@ -677,7 +677,6 @@ class Project < ApplicationRecord scope :non_archived, -> { where(archived: false) } scope :with_push, -> { joins(:events).merge(Event.pushed_action) } scope :with_project_feature, -> { joins('LEFT JOIN project_features ON projects.id = project_features.project_id') } - scope :with_jira_dvcs_cloud, -> { joins(:feature_usage).merge(ProjectFeatureUsage.with_jira_dvcs_integration_enabled(cloud: true)) } scope :with_jira_dvcs_server, -> { joins(:feature_usage).merge(ProjectFeatureUsage.with_jira_dvcs_integration_enabled(cloud: false)) } scope :inc_routes, -> { includes(:route, namespace: :route) } scope :with_statistics, -> { includes(:statistics) } diff --git a/app/models/project_feature_usage.rb b/app/models/project_feature_usage.rb index 5e47ec6310d..1f371156873 100644 --- a/app/models/project_feature_usage.rb +++ b/app/models/project_feature_usage.rb @@ -1,6 +1,9 @@ # frozen_string_literal: true class ProjectFeatureUsage < ApplicationRecord + include IgnorableColumns + ignore_column :jira_dvcs_cloud_last_sync_at, remove_with: '16.9', remove_after: '2024-01-21' + self.primary_key = :project_id JIRA_DVCS_CLOUD_FIELD = 'jira_dvcs_cloud_last_sync_at' diff --git a/app/models/repository.rb b/app/models/repository.rb index e639a389e0a..e4dba8a1911 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -1271,6 +1271,15 @@ class Repository .map(&:to_h) end + def filter_generated_files(revision, paths) + # NOTE: We should still support linguist-generated override for GitHub compatibility, + # but `gitlab-*` prefixed overrides would give us a better control moving forward. + generated_files = get_file_attributes(revision, paths, %w[gitlab-generated linguist-generated]) + .pluck(:path) + + paths - generated_files + end + private def ancestor_cache_key(ancestor_id, descendant_id) diff --git a/app/models/user.rb b/app/models/user.rb index 109b0506648..f6a455c280c 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -2064,7 +2064,11 @@ class User < MainClusterwide::ApplicationRecord def terms_accepted? return true if project_bot? || service_account? || security_policy_bot? - accepted_term_id.present? + if Feature.enabled?(:enforce_acceptance_of_changed_terms) + !!ApplicationSetting::Term.latest&.accepted_by_user?(self) + else + accepted_term_id.present? + end end def required_terms_not_accepted? diff --git a/app/models/vulnerability.rb b/app/models/vulnerability.rb index 1dff78354db..a9e1b2dbc5b 100644 --- a/app/models/vulnerability.rb +++ b/app/models/vulnerability.rb @@ -5,7 +5,7 @@ class Vulnerability < ApplicationRecord include EachBatch include IgnorableColumns - ignore_column %i[epic_id milestone_id], remove_with: '16.9', remove_after: '2023-01-13' + ignore_column %i[epic_id milestone_id last_edited_at], remove_with: '16.9', remove_after: '2023-01-13' alias_attribute :vulnerability_id, :id diff --git a/app/models/work_items/widgets/assignees.rb b/app/models/work_items/widgets/assignees.rb index 0707b03e647..839c82f8003 100644 --- a/app/models/work_items/widgets/assignees.rb +++ b/app/models/work_items/widgets/assignees.rb @@ -13,6 +13,10 @@ module WorkItems def self.quick_action_params [:assignee_ids] end + + def self.can_invite_members?(user, resource_parent) + user.can?("admin_#{resource_parent.to_ability_name}_member".to_sym, resource_parent) + end end end end diff --git a/app/policies/ci/runner_policy.rb b/app/policies/ci/runner_policy.rb index 7b01dccff87..0a9d9fdeae8 100644 --- a/app/policies/ci/runner_policy.rb +++ b/app/policies/ci/runner_policy.rb @@ -5,6 +5,7 @@ module Ci with_options scope: :subject, score: 0 condition(:locked, scope: :subject) { @subject.locked? } + with_options scope: :subject, score: 20 condition(:owned_runner) do @user.owns_runner?(@subject) end @@ -23,6 +24,11 @@ module Ci @subject.group_type? end + with_options scope: :subject, score: 0 + condition(:is_project_runner) do + @subject.project_type? + end + with_options scope: :user, score: 5 condition(:any_developer_maintainer_owned_groups_inheriting_shared_runners) do @user.developer_maintainer_owned_groups.with_shared_runners_enabled.any? @@ -44,6 +50,12 @@ module Ci end end + with_options score: 6 + condition(:developer_in_any_associated_projects) do + # Check if runner is associated to any projects where user is a developer+ + @subject.projects.visible_to_user_and_access_level(@user, Gitlab::Access::DEVELOPER).exists? + end + condition(:belongs_to_multiple_projects, scope: :subject) do @subject.belongs_to_more_than_one_project? end @@ -63,6 +75,10 @@ module Ci enable :read_runner end + rule { is_project_runner & developer_in_any_associated_projects }.policy do + enable :read_runner + end + rule { is_group_runner & any_associated_projects_in_group_runner_inheriting_group_runners }.policy do enable :read_runner end diff --git a/app/workers/bulk_imports/pipeline_worker.rb b/app/workers/bulk_imports/pipeline_worker.rb index 4bf6da73fdf..ee12096b835 100644 --- a/app/workers/bulk_imports/pipeline_worker.rb +++ b/app/workers/bulk_imports/pipeline_worker.rb @@ -62,7 +62,7 @@ module BulkImports @entity = ::BulkImports::Entity.find(entity_id) @pipeline_tracker = ::BulkImports::Tracker.find(pipeline_tracker_id) - fail_tracker(exception) + fail_pipeline(exception) end private @@ -100,9 +100,11 @@ module BulkImports entity.bulk_import.source_version_info.to_s end - def fail_tracker(exception) + def fail_pipeline(exception) pipeline_tracker.update!(status_event: 'fail_op', jid: jid) + entity.fail_op! if pipeline_tracker.abort_on_failure? + log_exception(exception, log_attributes(message: 'Pipeline failed')) Gitlab::ErrorTracking.track_exception(exception, log_attributes) diff --git a/app/workers/concerns/gitlab/github_import/stage_methods.rb b/app/workers/concerns/gitlab/github_import/stage_methods.rb index 5c63c667a03..14a2d859c14 100644 --- a/app/workers/concerns/gitlab/github_import/stage_methods.rb +++ b/app/workers/concerns/gitlab/github_import/stage_methods.rb @@ -37,8 +37,6 @@ module Gitlab # - Continue their loop from where it left off: # https://gitlab.com/gitlab-org/gitlab/-/blob/024235ec/lib/gitlab/github_import/importer/pull_requests/review_requests_importer.rb#L15 def resumes_work_when_interrupted! - return unless Feature.enabled?(:github_importer_raise_max_interruptions) - sidekiq_options max_retries_after_interruption: MAX_RETRIES_AFTER_INTERRUPTION end end diff --git a/config/feature_flags/development/github_importer_raise_max_interruptions.yml b/config/feature_flags/development/enforce_acceptance_of_changed_terms.yml index 3cbcc10865f..5864d25bc89 100644 --- a/config/feature_flags/development/github_importer_raise_max_interruptions.yml +++ b/config/feature_flags/development/enforce_acceptance_of_changed_terms.yml @@ -1,8 +1,8 @@ --- -name: github_importer_raise_max_interruptions -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/134949 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/429306 -milestone: '16.6' +name: enforce_acceptance_of_changed_terms +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/136039 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/430799 +milestone: '16.7' type: development -group: group::import and integrate +group: group::authentication default_enabled: false diff --git a/config/feature_flags/development/only_highlight_discussions_requested.yml b/config/feature_flags/development/log_read_namespace_usages.yml index 8dfb93c33e0..fd844a9c9f5 100644 --- a/config/feature_flags/development/only_highlight_discussions_requested.yml +++ b/config/feature_flags/development/log_read_namespace_usages.yml @@ -1,8 +1,8 @@ --- -name: only_highlight_discussions_requested -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/135096 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/429489 -milestone: '16.6' +name: log_read_namespace_usages +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/136617 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/421150 +milestone: '16.7' type: development -group: group::code review +group: group::tenant scale default_enabled: false diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb index 9b6a9b17935..8df12671f26 100644 --- a/config/initializers/sidekiq.rb +++ b/config/initializers/sidekiq.rb @@ -85,12 +85,6 @@ Sidekiq.configure_server do |config| end if enable_reliable_fetch? - if Gitlab::Utils.to_boolean(ENV['SIDEKIQ_ENABLE_DUAL_NAMESPACE_POLLING'], default: true) - # set non-namespaced store for fetcher to poll both namespaced and non-namespaced queues - config[:alternative_store] = ::Gitlab::Redis::Queues - config[:namespace] = Gitlab::Redis::Queues::SIDEKIQ_NAMESPACE - end - config[:semi_reliable_fetch] = enable_semi_reliable_fetch_mode? Sidekiq::ReliableFetch.setup_reliable_fetch!(config) end @@ -119,7 +113,6 @@ Sidekiq.configure_client do |config| config.client_middleware(&Gitlab::SidekiqMiddleware.client_configurator) end -Sidekiq::Scheduled::Enq.prepend Gitlab::Patch::SidekiqScheduledEnq Sidekiq::Scheduled::Poller.prepend Gitlab::Patch::SidekiqPoller Sidekiq::Cron::Poller.prepend Gitlab::Patch::SidekiqPoller Sidekiq::Cron::Poller.prepend Gitlab::Patch::SidekiqCronPoller diff --git a/config/metrics/counts_28d/20210216181152_projects_jira_dvcs_cloud_active.yml b/config/metrics/counts_28d/20210216181152_projects_jira_dvcs_cloud_active.yml index 3169c02624a..650e84cca79 100644 --- a/config/metrics/counts_28d/20210216181152_projects_jira_dvcs_cloud_active.yml +++ b/config/metrics/counts_28d/20210216181152_projects_jira_dvcs_cloud_active.yml @@ -7,7 +7,7 @@ product_section: dev product_stage: manage product_group: integration value_type: number -status: active +status: removed time_frame: 28d data_source: database distribution: @@ -20,3 +20,5 @@ tier: performance_indicator_type: - customer_health_score milestone: "<13.9" +removed_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/136484 +milestone_removed: "16.7" diff --git a/config/metrics/counts_28d/20220825232557_count_user_auth.yml b/config/metrics/counts_28d/20220825232557_count_user_auth.yml index 7fd9a845130..625973f9851 100644 --- a/config/metrics/counts_28d/20220825232557_count_user_auth.yml +++ b/config/metrics/counts_28d/20220825232557_count_user_auth.yml @@ -12,8 +12,6 @@ time_frame: 28d data_source: database instrumentation_class: CountUserAuthMetric data_category: optional -performance_indicator_type: -- smau distribution: - ce - ee diff --git a/config/metrics/counts_7d/20220615103711_incident_management_timeline_event_total_unique_counts_weekly.yml b/config/metrics/counts_7d/20220615103711_incident_management_timeline_event_total_unique_counts_weekly.yml index 20c36d459c3..da73a2b5412 100644 --- a/config/metrics/counts_7d/20220615103711_incident_management_timeline_event_total_unique_counts_weekly.yml +++ b/config/metrics/counts_7d/20220615103711_incident_management_timeline_event_total_unique_counts_weekly.yml @@ -20,9 +20,7 @@ options: - incident_management_timeline_event_created - incident_management_timeline_event_edited - incident_management_timeline_event_deleted -performance_indicator_type: - - gmau - - paid_gmau +performance_indicator_type: [] distribution: - ce - ee diff --git a/config/metrics/counts_all/20210216181128_projects_jira_dvcs_cloud_active.yml b/config/metrics/counts_all/20210216181128_projects_jira_dvcs_cloud_active.yml index 015a7c2bbbe..7b89554c2b2 100644 --- a/config/metrics/counts_all/20210216181128_projects_jira_dvcs_cloud_active.yml +++ b/config/metrics/counts_all/20210216181128_projects_jira_dvcs_cloud_active.yml @@ -7,7 +7,7 @@ product_section: dev product_stage: manage product_group: integration value_type: number -status: active +status: removed time_frame: all data_source: database distribution: @@ -18,3 +18,5 @@ tier: - premium - ultimate milestone: "<13.9" +removed_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/136484 +milestone_removed: "16.7" diff --git a/doc/.vale/gitlab/Prerequisites.yml b/doc/.vale/gitlab/Prerequisites.yml new file mode 100644 index 00000000000..239f9277c4d --- /dev/null +++ b/doc/.vale/gitlab/Prerequisites.yml @@ -0,0 +1,14 @@ +--- +# Error: gitlab.Prerequisites +# +# The "Prerequisites:" line should always be plural. +# +# For a list of all options, see https://vale.sh/docs/topics/styles/ +extends: existence +message: "Pluralize 'Prerequisites', even if it includes only one item." +link: https://docs.gitlab.com/ee/development/documentation/topic_types/task.html#task-prerequisites +level: warning +nonword: true +scope: text +raw: + - '^Prerequisite:' diff --git a/doc/administration/settings/usage_statistics.md b/doc/administration/settings/usage_statistics.md index 05133d2d5f0..b8e8d96b88e 100644 --- a/doc/administration/settings/usage_statistics.md +++ b/doc/administration/settings/usage_statistics.md @@ -72,6 +72,7 @@ In the following table, you can see: | [Advanced search](../../user/search/advanced_search.md) | GitLab 16.6 and later | | [DevOps Adoption](../../user/group/devops_adoption/index.md) | GitLab 16.6 and later | | [Сross-project pipelines with artifacts dependencies](../../ci/yaml/index.md#needsproject) | GitLab 16.7 and later | +| [Feature flag related issues](../../operations/feature_flags.md#feature-flag-related-issues) | GitLab 16.7 and later | ### Enable registration features diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index ad97272efff..4c64e6521c4 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -27752,6 +27752,7 @@ Check permissions for the current user on a work item. | <a id="workitemtypeiconname"></a>`iconName` | [`String`](#string) | Icon name of the work item type. | | <a id="workitemtypeid"></a>`id` | [`WorkItemsTypeID!`](#workitemstypeid) | Global ID of the work item type. | | <a id="workitemtypename"></a>`name` | [`String!`](#string) | Name of the work item type. | +| <a id="workitemtypewidgetdefinitions"></a>`widgetDefinitions` **{warning-solid}** | [`[WorkItemWidgetDefinition!]`](#workitemwidgetdefinition) | **Introduced** in 16.7. This feature is an Experiment. It can be changed or removed at any time. Available widgets for the work item type. | ### `WorkItemWidgetAssignees` @@ -27807,6 +27808,27 @@ four standard [pagination arguments](#connection-pagination-arguments): | ---- | ---- | ----------- | | <a id="workitemwidgetcurrentusertodoscurrentusertodosstate"></a>`state` | [`TodoStateEnum`](#todostateenum) | State of the to-do items. | +### `WorkItemWidgetDefinitionAssignees` + +Represents an assignees widget definition. + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="workitemwidgetdefinitionassigneescaninvitemembers"></a>`canInviteMembers` | [`Boolean!`](#boolean) | Indicates whether the current user can invite members to the work item's parent. | +| <a id="workitemwidgetdefinitionassigneestype"></a>`type` | [`WorkItemWidgetType!`](#workitemwidgettype) | Widget type. | + +### `WorkItemWidgetDefinitionGeneric` + +Represents a generic widget definition. + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="workitemwidgetdefinitiongenerictype"></a>`type` | [`WorkItemWidgetType!`](#workitemwidgettype) | Widget type. | + ### `WorkItemWidgetDescription` Represents a description widget. @@ -32487,6 +32509,19 @@ Implementations: | ---- | ---- | ----------- | | <a id="workitemwidgettype"></a>`type` | [`WorkItemWidgetType`](#workitemwidgettype) | Widget type. | +#### `WorkItemWidgetDefinition` + +Implementations: + +- [`WorkItemWidgetDefinitionAssignees`](#workitemwidgetdefinitionassignees) +- [`WorkItemWidgetDefinitionGeneric`](#workitemwidgetdefinitiongeneric) + +##### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="workitemwidgetdefinitiontype"></a>`type` | [`WorkItemWidgetType!`](#workitemwidgettype) | Widget type. | + ## Input types Types that may be used as arguments (all scalar types may also diff --git a/doc/api/project_vulnerabilities.md b/doc/api/project_vulnerabilities.md index 2b4d3ec50df..1fbea66de7f 100644 --- a/doc/api/project_vulnerabilities.md +++ b/doc/api/project_vulnerabilities.md @@ -7,7 +7,8 @@ type: reference, api # Project Vulnerabilities API **(ULTIMATE ALL)** -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/10242) in GitLab 12.6. +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/10242) in GitLab 12.6. +> - `last_edited_at` [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/268154) in GitLab 16.7. WARNING: This API is in the process of being deprecated and considered unstable. @@ -79,7 +80,6 @@ Example response: "vulnerability_id": 103 }, "id": 103, - "last_edited_at": null, "last_edited_by_id": null, "project": { "created_at": "2020-04-07T13:54:25.634Z", @@ -167,7 +167,6 @@ Example response: "vulnerability_id": 103 }, "id": 103, - "last_edited_at": null, "last_edited_by_id": null, "project": { "created_at": "2020-04-07T13:54:25.634Z", diff --git a/doc/api/vulnerabilities.md b/doc/api/vulnerabilities.md index dc5e5c5f509..ff45c5d6cfa 100644 --- a/doc/api/vulnerabilities.md +++ b/doc/api/vulnerabilities.md @@ -6,7 +6,8 @@ info: To determine the technical writer assigned to the Stage/Group associated w # Vulnerabilities API **(ULTIMATE ALL)** -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/10242) in GitLab 12.6. +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/10242) in GitLab 12.6. +> - `last_edited_at` [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/268154) in GitLab 16.7. NOTE: The former Vulnerabilities API was renamed to Vulnerability Findings API @@ -67,7 +68,6 @@ Example response: "due_date": null, "created_at": "2019-10-13T15:08:40.219Z", "updated_at": "2019-10-13T15:09:40.382Z", - "last_edited_at": null, "closed_at": null } ``` @@ -117,7 +117,6 @@ Example response: "due_date": null, "created_at": "2019-10-13T15:08:40.219Z", "updated_at": "2019-10-13T15:09:40.382Z", - "last_edited_at": null, "closed_at": null } ``` @@ -167,7 +166,6 @@ Example response: "due_date": null, "created_at": "2019-10-13T15:08:40.219Z", "updated_at": "2019-10-13T15:09:40.382Z", - "last_edited_at": null, "closed_at": null } ``` @@ -217,7 +215,6 @@ Example response: "due_date": null, "created_at": "2019-10-13T15:08:40.219Z", "updated_at": "2019-10-13T15:09:40.382Z", - "last_edited_at": null, "closed_at": null } ``` @@ -267,7 +264,6 @@ Example response: "due_date": null, "created_at": "2019-10-13T15:08:40.219Z", "updated_at": "2019-10-13T15:09:40.382Z", - "last_edited_at": null, "closed_at": null } ``` diff --git a/doc/architecture/blueprints/google_cloud_platform_integration/index.md b/doc/architecture/blueprints/google_cloud_platform_integration/index.md new file mode 100644 index 00000000000..7fe2c1f655a --- /dev/null +++ b/doc/architecture/blueprints/google_cloud_platform_integration/index.md @@ -0,0 +1,34 @@ +--- +status: ongoing +creation-date: "2023-10-26" +authors: [ "@sgoldstein" ] +coaches: ["@jessieay", "@grzesiek"] +approvers: ["@sgoldstein", "@jreporter"] +owning-stage: "~section::ops" +participating-stages: ["~devops::verify", "~devops::package", "~devops::govern"] +--- + +# Google Cloud Platform Integration + +GitLab and Google Cloud Platform (GCP) provide complementary tooling which we +are integrating via our [partnership](https://about.gitlab.com/blog/2023/08/29/gitlab-google-partnership-s3c/). + +This design doc is not public at that time. The whole content is +available in [GitLab-internal project](https://gitlab.com/gitlab-org/architecture/gitlab-gcp-integration/design-doc). + +## Who + +<!-- vale gitlab.Spelling = NO --> + +| Who | Role +|------------------------|--------------------------------------------------| +| Sam Goldstein | Director of Engineering, Engineering DRI | +| Grzegorz Bizon | Distinguished Engineer - Technical Lead | +| Jessie Young | Principal Engineer | +| David Fernandez | Staff Engineer | +| Imre Farkas | Staff Engineer | +| João Pereira | Staff Engineer | +| Joe Burnett | Staff Engineer | +| Tomasz Maczukin | Senior Engineer | + +<!-- vale gitlab.Spelling = YES --> diff --git a/doc/development/internal_analytics/index.md b/doc/development/internal_analytics/index.md index 4a719cf43b9..b5403f56600 100644 --- a/doc/development/internal_analytics/index.md +++ b/doc/development/internal_analytics/index.md @@ -97,6 +97,7 @@ FROM common_mart.mart_behavior_structured_event WHERE event_action = 'feature_used' AND event_category = 'InternalEventTracking' AND behavior_date > '2023-08-01' --restricted minimum date for performance +AND app_id='gitlab' -- use gitlab for production events and gitlab-staging for events from staging GROUP BY 1 ORDER BY 1 desc ``` diff --git a/lib/backup/repositories.rb b/lib/backup/repositories.rb index 46825dbd203..c3154ccfbb5 100644 --- a/lib/backup/repositories.rb +++ b/lib/backup/repositories.rb @@ -38,7 +38,6 @@ module Backup ensure strategy.finish! - cleanup_snippets_without_repositories restore_object_pools end @@ -133,24 +132,6 @@ module Backup pool.schedule end end - - # Snippets without a repository should be removed because they failed to import - # due to having invalid repositories - def cleanup_snippets_without_repositories - invalid_snippets = [] - - snippet_relation.find_each(batch_size: 1000).each do |snippet| - response = Snippets::RepositoryValidationService.new(nil, snippet).execute - next if response.success? - - snippet.repository.remove - progress.puts("Snippet #{snippet.full_path} can't be restored: #{response.message}") - - invalid_snippets << snippet.id - end - - Snippet.id_in(invalid_snippets).delete_all - end end end diff --git a/lib/bulk_imports/common/pipelines/entity_finisher.rb b/lib/bulk_imports/common/pipelines/entity_finisher.rb index 723359aa438..a8bbd122445 100644 --- a/lib/bulk_imports/common/pipelines/entity_finisher.rb +++ b/lib/bulk_imports/common/pipelines/entity_finisher.rb @@ -8,6 +8,10 @@ module BulkImports false end + def self.abort_on_failure? + false + end + def initialize(context) @context = context @entity = @context.entity diff --git a/lib/gitlab/patch/sidekiq_scheduled_enq.rb b/lib/gitlab/patch/sidekiq_scheduled_enq.rb deleted file mode 100644 index b5a40c19923..00000000000 --- a/lib/gitlab/patch/sidekiq_scheduled_enq.rb +++ /dev/null @@ -1,33 +0,0 @@ -# frozen_string_literal: true - -# Patch to address https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/2286 -# Using a dual-namespace poller eliminates the need for script based migration of -# schedule-related sets in Sidekiq. -module Gitlab - module Patch - module SidekiqScheduledEnq - # The patched enqueue_jobs will poll non-namespaced scheduled sets before doing the same for - # namespaced sets via super and vice-versa depending on how Sidekiq.redis was configured - def enqueue_jobs(sorted_sets = Sidekiq::Scheduled::SETS) - # checks the other namespace - if Gitlab::Utils.to_boolean(ENV['SIDEKIQ_ENABLE_DUAL_NAMESPACE_POLLING'], default: true) - # Refer to https://github.com/sidekiq/sidekiq/blob/v6.5.7/lib/sidekiq/scheduled.rb#L25 - # this portion swaps out Sidekiq.redis for Gitlab::Redis::Queues - Gitlab::Redis::Queues.with do |conn| # rubocop:disable Cop/RedisQueueUsage - sorted_sets.each do |sorted_set| - # adds namespace since `super` polls with a non-namespaced Sidekiq.redis - sorted_set = "#{Gitlab::Redis::Queues::SIDEKIQ_NAMESPACE}:#{sorted_set}" # rubocop:disable Cop/RedisQueueUsage - - while !@done && (job = zpopbyscore(conn, keys: [sorted_set], argv: [Time.now.to_f.to_s])) # rubocop:disable Gitlab/ModuleWithInstanceVariables, Lint/AssignmentInCondition - Sidekiq::Client.push(Sidekiq.load_json(job)) # rubocop:disable Cop/SidekiqApiUsage - Sidekiq.logger.debug { "enqueued #{sorted_set}: #{job}" } - end - end - end - end - - super - end - end - end -end diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb index 2b8b7aedeb2..41fb946c62d 100644 --- a/lib/gitlab/usage_data.rb +++ b/lib/gitlab/usage_data.rb @@ -406,7 +406,6 @@ module Gitlab service_desk_enabled_projects: distinct_count_service_desk_enabled_projects(time_period), service_desk_issues: count(::Issue.service_desk.where(time_period)), projects_jira_active: distinct_count(::Project.with_active_integration(::Integrations::Jira).where(time_period), :creator_id), - projects_jira_dvcs_cloud_active: distinct_count(::Project.with_active_integration(::Integrations::Jira).with_jira_dvcs_cloud.where(time_period), :creator_id), projects_jira_dvcs_server_active: distinct_count(::Project.with_active_integration(::Integrations::Jira).with_jira_dvcs_server.where(time_period), :creator_id) } end diff --git a/package.json b/package.json index ec0af3047d7..bd1541ee58c 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "@gitlab/favicon-overlay": "2.0.0", "@gitlab/fonts": "^1.3.0", "@gitlab/svgs": "3.71.0", - "@gitlab/ui": "^68.5.0", + "@gitlab/ui": "^69.0.0", "@gitlab/visual-review-tools": "1.7.3", "@gitlab/web-ide": "^0.0.1-dev-20231116214726", "@mattiasbuelens/web-streams-adapter": "^0.1.0", diff --git a/qa/qa/page/component/members/invite_members_modal.rb b/qa/qa/page/component/members/invite_members_modal.rb index e3d681cf403..36bff71aa38 100644 --- a/qa/qa/page/component/members/invite_members_modal.rb +++ b/qa/qa/page/component/members/invite_members_modal.rb @@ -12,37 +12,37 @@ module QA super base.view 'app/assets/javascripts/invite_members/components/invite_modal_base.vue' do - element :invite_button - element :access_level_dropdown + element 'invite-modal-submit' + element 'access-level-dropdown' element 'invite-modal' end base.view 'app/assets/javascripts/invite_members/components/members_token_select.vue' do - element :members_token_select_input + element 'members-token-select-input' end base.view 'app/assets/javascripts/invite_members/components/invite_group_trigger.vue' do - element :invite_a_group_button + element 'invite-group-button' end base.view 'app/assets/javascripts/invite_members/constants.js' do - element :invite_members_button + element 'invite-members-button' end end def open_invite_members_modal - click_element :invite_members_button + click_element 'invite-members-button' end def open_invite_group_modal - click_element :invite_a_group_button + click_element 'invite-group-button' end def add_member(username, access_level = 'Developer', refresh_page: true) open_invite_members_modal within_element('invite-modal') do - fill_element(:members_token_select_input, username) + fill_element('members-token-select-input', username) Support::WaitForRequests.wait_for_requests click_button(username, match: :prefer_exact) set_access_level(access_level) @@ -68,7 +68,7 @@ module QA end def send_invite(refresh = false) - click_element :invite_button + click_element 'invite-modal-submit' Support::WaitForRequests.wait_for_requests page.refresh if refresh end @@ -76,7 +76,7 @@ module QA private def set_access_level(access_level) - within_element(:access_level_dropdown) do + within_element('access-level-dropdown') do expand_select_list select_item access_level 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 72569d1ac73..e140b37e137 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 @@ -367,7 +367,14 @@ module QA logger.info("== Verifying repository import ==") expect(imported_project.description).to eq(gh_repo.description) expect(gl_branches).to include(*gh_branches) - expect(gl_commits).to include(*gh_commits) + + # When testing with very large repositories, comparing with include will raise 'stack level too deep' error + # Compare just the size in this case + if gh_commits.size > 10000 + expect(gl_commits.size).to be >= gh_commits.size + else + expect(gl_commits).to include(*gh_commits) + end end # Verify imported labels diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index b7ee01ce6b3..047dd12912b 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -470,7 +470,7 @@ RSpec.describe ApplicationController, feature_category: :shared do enforce_terms - expect { get :index }.not_to exceed_query_limit(control) + expect { get :index }.not_to exceed_query_limit(control.count).with_threshold(1) end context 'when terms are enforced' do diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index 1e879458f48..55741a82862 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -2282,121 +2282,58 @@ RSpec.describe Projects::MergeRequestsController, feature_category: :code_review end context 'highlight preloading' do - context 'when only_highlight_discussions_requested is false' do - before do - stub_feature_flags(only_highlight_discussions_requested: false) + context 'with commit diff notes' do + let!(:first_commit_diff_note) do + create(:diff_note_on_commit, project: merge_request.project) end - context 'with commit diff notes' do - let!(:first_commit_diff_note) do - create(:diff_note_on_commit, project: merge_request.project) - end - - let!(:second_commit_diff_note) do - create(:diff_note_on_commit, project: merge_request.project) - end - - it 'preloads all of the notes diffs highlights' do - expect_next_instance_of(Gitlab::DiscussionsDiff::FileCollection) do |collection| - first_note_diff_file = first_commit_diff_note.note_diff_file - second_note_diff_file = second_commit_diff_note.note_diff_file + let!(:second_commit_diff_note) do + create(:diff_note_on_commit, project: merge_request.project) + end - expect(collection).to receive(:load_highlight).and_call_original - expect(collection).to receive(:find_by_id).with(first_note_diff_file.id).and_call_original - expect(collection).to receive(:find_by_id).with(second_note_diff_file.id).and_call_original - end + it 'preloads all of the notes diffs highlights' do + expect_next_instance_of(Gitlab::DiscussionsDiff::FileCollection) do |collection| + first_note_diff_file = first_commit_diff_note.note_diff_file + second_note_diff_file = second_commit_diff_note.note_diff_file - get :discussions, params: { namespace_id: project.namespace, project_id: project, id: merge_request.iid, - per_page: 2 } + expect(collection).to receive(:load_highlight).with(diff_note_ids: [first_commit_diff_note.id, second_commit_diff_note.id]).and_call_original + expect(collection).to receive(:find_by_id).with(first_note_diff_file.id).and_call_original + expect(collection).to receive(:find_by_id).with(second_note_diff_file.id).and_call_original end - it 'preloads all of the notes diffs highlights when per_page is 1' do - expect_next_instance_of(Gitlab::DiscussionsDiff::FileCollection) do |collection| - first_note_diff_file = first_commit_diff_note.note_diff_file - second_note_diff_file = second_commit_diff_note.note_diff_file - - expect(collection).to receive(:load_highlight).and_call_original - expect(collection).to receive(:find_by_id).with(first_note_diff_file.id).and_call_original - expect(collection).not_to receive(:find_by_id).with(second_note_diff_file.id) - end - - get :discussions, params: { namespace_id: project.namespace, project_id: project, id: merge_request.iid, - per_page: 1 } - end + get :discussions, params: { namespace_id: project.namespace, project_id: project, id: merge_request.iid, + per_page: 2 } end - context 'with diff notes' do - let!(:diff_note) do - create(:diff_note_on_merge_request, noteable: merge_request, project: merge_request.project) - end - - it 'preloads notes diffs highlights' do - expect_next_instance_of(Gitlab::DiscussionsDiff::FileCollection) do |collection| - note_diff_file = diff_note.note_diff_file + it 'preloads all of the notes diffs highlights when per_page is 1' do + expect_next_instance_of(Gitlab::DiscussionsDiff::FileCollection) do |collection| + first_note_diff_file = first_commit_diff_note.note_diff_file + second_note_diff_file = second_commit_diff_note.note_diff_file - expect(collection).to receive(:load_highlight).and_call_original - expect(collection).to receive(:find_by_id).with(note_diff_file.id).and_call_original - end - - get :discussions, params: { namespace_id: project.namespace, project_id: project, id: merge_request.iid } + expect(collection).to receive(:load_highlight).with(diff_note_ids: [first_commit_diff_note.id]).and_call_original + expect(collection).to receive(:find_by_id).with(first_note_diff_file.id).and_call_original + expect(collection).not_to receive(:find_by_id).with(second_note_diff_file.id) end + + get :discussions, params: { namespace_id: project.namespace, project_id: project, id: merge_request.iid, + per_page: 1 } end end - context 'when only_highlight_discussions_requested is true' do - context 'with commit diff notes' do - let!(:first_commit_diff_note) do - create(:diff_note_on_commit, project: merge_request.project) - end - - let!(:second_commit_diff_note) do - create(:diff_note_on_commit, project: merge_request.project) - end - - it 'preloads all of the notes diffs highlights' do - expect_next_instance_of(Gitlab::DiscussionsDiff::FileCollection) do |collection| - first_note_diff_file = first_commit_diff_note.note_diff_file - second_note_diff_file = second_commit_diff_note.note_diff_file - - expect(collection).to receive(:load_highlight).with(diff_note_ids: [first_commit_diff_note.id, second_commit_diff_note.id]).and_call_original - expect(collection).to receive(:find_by_id).with(first_note_diff_file.id).and_call_original - expect(collection).to receive(:find_by_id).with(second_note_diff_file.id).and_call_original - end - - get :discussions, params: { namespace_id: project.namespace, project_id: project, id: merge_request.iid, - per_page: 2 } - end - - it 'preloads all of the notes diffs highlights when per_page is 1' do - expect_next_instance_of(Gitlab::DiscussionsDiff::FileCollection) do |collection| - first_note_diff_file = first_commit_diff_note.note_diff_file - second_note_diff_file = second_commit_diff_note.note_diff_file - - expect(collection).to receive(:load_highlight).with(diff_note_ids: [first_commit_diff_note.id]).and_call_original - expect(collection).to receive(:find_by_id).with(first_note_diff_file.id).and_call_original - expect(collection).not_to receive(:find_by_id).with(second_note_diff_file.id) - end - - get :discussions, params: { namespace_id: project.namespace, project_id: project, id: merge_request.iid, - per_page: 1 } - end + context 'with diff notes' do + let!(:diff_note) do + create(:diff_note_on_merge_request, noteable: merge_request, project: merge_request.project) end - context 'with diff notes' do - let!(:diff_note) do - create(:diff_note_on_merge_request, noteable: merge_request, project: merge_request.project) - end + it 'preloads notes diffs highlights' do + expect_next_instance_of(Gitlab::DiscussionsDiff::FileCollection) do |collection| + note_diff_file = diff_note.note_diff_file - it 'preloads notes diffs highlights' do - expect_next_instance_of(Gitlab::DiscussionsDiff::FileCollection) do |collection| - note_diff_file = diff_note.note_diff_file - - expect(collection).to receive(:load_highlight).with(diff_note_ids: [diff_note.id]).and_call_original - expect(collection).to receive(:find_by_id).with(note_diff_file.id).and_call_original - end - - get :discussions, params: { namespace_id: project.namespace, project_id: project, id: merge_request.iid } + expect(collection).to receive(:load_highlight).with(diff_note_ids: [diff_note.id]).and_call_original + expect(collection).to receive(:find_by_id).with(note_diff_file.id).and_call_original end + + get :discussions, params: { namespace_id: project.namespace, project_id: project, id: merge_request.iid } end end end diff --git a/spec/factories/project_feature_usage.rb b/spec/factories/project_feature_usage.rb index 8265ea04392..eb9605d08d6 100644 --- a/spec/factories/project_feature_usage.rb +++ b/spec/factories/project_feature_usage.rb @@ -4,10 +4,6 @@ FactoryBot.define do factory :project_feature_usage do project - trait :dvcs_cloud do - jira_dvcs_cloud_last_sync_at { Time.current } - end - trait :dvcs_server do jira_dvcs_server_last_sync_at { Time.current } end diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index 1e3ade779af..a9c3285356a 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -177,12 +177,6 @@ FactoryBot.define do import_status { :canceled } end - trait :jira_dvcs_cloud do - before(:create) do |project| - create(:project_feature_usage, :dvcs_cloud, project: project) - end - end - trait :jira_dvcs_server do before(:create) do |project| create(:project_feature_usage, :dvcs_server, project: project) diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb index 4e0198b1f2b..a443ba62abb 100644 --- a/spec/features/admin/admin_settings_spec.rb +++ b/spec/features/admin/admin_settings_spec.rb @@ -261,7 +261,7 @@ RSpec.describe 'Admin updates settings', feature_category: :shared do expect(page).to have_content "Application settings saved successfully" end - it 'terms of Service' do + it 'terms of Service', :js do # Already have the admin accept terms, so they don't need to accept in this spec. _existing_terms = create(:term) accept_terms(admin) @@ -274,7 +274,10 @@ RSpec.describe 'Admin updates settings', feature_category: :shared do expect(current_settings.enforce_terms).to be(true) expect(current_settings.terms).to eq 'Be nice!' - expect(page).to have_content 'Application settings saved successfully' + + click_button 'Accept terms' + + expect(page).to have_current_path(general_admin_application_settings_path, ignore_query: true) end it 'modify oauth providers' do diff --git a/spec/features/projects/members/manage_groups_spec.rb b/spec/features/projects/members/manage_groups_spec.rb index 63ff1ba8455..7bd7c822a5d 100644 --- a/spec/features/projects/members/manage_groups_spec.rb +++ b/spec/features/projects/members/manage_groups_spec.rb @@ -228,6 +228,6 @@ RSpec.describe 'Project > Members > Manage groups', :js, feature_category: :grou end def invite_group_selector - 'button[data-test-id="invite-group-button"]' + 'button[data-testid="invite-group-button"]' end end diff --git a/spec/frontend/commons/nav/user_merge_requests_spec.js b/spec/frontend/commons/nav/user_merge_requests_spec.js deleted file mode 100644 index 114cbbf812c..00000000000 --- a/spec/frontend/commons/nav/user_merge_requests_spec.js +++ /dev/null @@ -1,154 +0,0 @@ -import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; -import * as UserApi from '~/api/user_api'; -import { - openUserCountsBroadcast, - closeUserCountsBroadcast, - refreshUserMergeRequestCounts, -} from '~/commons/nav/user_merge_requests'; - -jest.mock('~/api'); - -const TEST_COUNT = 1000; -const MR_COUNT_CLASS = 'js-merge-requests-count'; - -describe('User Merge Requests', () => { - let channelMock; - let newBroadcastChannelMock; - - beforeEach(() => { - jest.spyOn(document, 'dispatchEvent').mockReturnValue(false); - - global.gon.current_user_id = 123; - global.gon.use_new_navigation = false; - - channelMock = { - postMessage: jest.fn(), - close: jest.fn(), - }; - newBroadcastChannelMock = jest.fn().mockImplementation(() => channelMock); - - global.BroadcastChannel = newBroadcastChannelMock; - setHTMLFixture( - `<div><div class="${MR_COUNT_CLASS}">0</div><div class="js-assigned-mr-count"></div><div class="js-reviewer-mr-count"></div></div>`, - ); - }); - - afterEach(() => { - resetHTMLFixture(); - }); - - const findMRCountText = () => document.body.querySelector(`.${MR_COUNT_CLASS}`).textContent; - - describe('refreshUserMergeRequestCounts', () => { - beforeEach(() => { - jest.spyOn(UserApi, 'getUserCounts').mockResolvedValue({ - data: { - assigned_merge_requests: TEST_COUNT, - review_requested_merge_requests: TEST_COUNT, - }, - }); - }); - - describe('with open broadcast channel', () => { - beforeEach(() => { - openUserCountsBroadcast(); - - return refreshUserMergeRequestCounts(); - }); - - it('updates the top count of merge requests', () => { - expect(findMRCountText()).toEqual(Number(TEST_COUNT + TEST_COUNT).toLocaleString()); - }); - - it('calls the API', () => { - expect(UserApi.getUserCounts).toHaveBeenCalled(); - }); - - it('posts count to BroadcastChannel', () => { - expect(channelMock.postMessage).toHaveBeenCalledWith(TEST_COUNT + TEST_COUNT); - }); - }); - - describe('without open broadcast channel', () => { - beforeEach(() => refreshUserMergeRequestCounts()); - - it('does not post anything', () => { - expect(channelMock.postMessage).not.toHaveBeenCalled(); - }); - }); - - it('does not emit event to refetch counts', () => { - expect(document.dispatchEvent).not.toHaveBeenCalled(); - }); - }); - - describe('openUserCountsBroadcast', () => { - beforeEach(() => { - openUserCountsBroadcast(); - }); - - it('creates BroadcastChannel that updates DOM on message received', () => { - expect(findMRCountText()).toEqual('0'); - - channelMock.onmessage({ data: TEST_COUNT }); - - expect(newBroadcastChannelMock).toHaveBeenCalled(); - expect(findMRCountText()).toEqual(TEST_COUNT.toLocaleString()); - }); - - it('closes if called while already open', () => { - expect(channelMock.close).not.toHaveBeenCalled(); - - openUserCountsBroadcast(); - - expect(newBroadcastChannelMock).toHaveBeenCalled(); - expect(channelMock.close).toHaveBeenCalled(); - }); - }); - - describe('closeUserCountsBroadcast', () => { - describe('when not opened', () => { - it('does nothing', () => { - expect(channelMock.close).not.toHaveBeenCalled(); - }); - }); - - describe('when opened', () => { - beforeEach(() => { - openUserCountsBroadcast(); - }); - - it('closes', () => { - expect(channelMock.close).not.toHaveBeenCalled(); - - closeUserCountsBroadcast(); - - expect(channelMock.close).toHaveBeenCalled(); - }); - }); - }); - - describe('if new navigation is enabled', () => { - beforeEach(() => { - global.gon.use_new_navigation = true; - jest.spyOn(UserApi, 'getUserCounts'); - }); - - it('openUserCountsBroadcast is a noop', () => { - openUserCountsBroadcast(); - expect(newBroadcastChannelMock).not.toHaveBeenCalled(); - }); - - describe('refreshUserMergeRequestCounts', () => { - it('does not call api', async () => { - await refreshUserMergeRequestCounts(); - expect(UserApi.getUserCounts).not.toHaveBeenCalled(); - }); - - it('emits event to refetch counts', async () => { - await refreshUserMergeRequestCounts(); - expect(document.dispatchEvent).toHaveBeenCalledWith(new CustomEvent('todo:toggle')); - }); - }); - }); -}); diff --git a/spec/frontend/invite_members/components/invite_members_trigger_spec.js b/spec/frontend/invite_members/components/invite_members_trigger_spec.js index 58c40a49b3c..c8a06a66a3f 100644 --- a/spec/frontend/invite_members/components/invite_members_trigger_spec.js +++ b/spec/frontend/invite_members/components/invite_members_trigger_spec.js @@ -4,7 +4,7 @@ import InviteMembersTrigger from '~/invite_members/components/invite_members_tri import eventHub from '~/invite_members/event_hub'; import { TRIGGER_ELEMENT_BUTTON, - TRIGGER_DEFAULT_QA_SELECTOR, + TRIGGER_DEFAULT_TESTID, TRIGGER_ELEMENT_WITH_EMOJI, TRIGGER_ELEMENT_DROPDOWN_WITH_EMOJI, TRIGGER_ELEMENT_DISCLOSURE_DROPDOWN, @@ -67,16 +67,16 @@ describe.each(triggerItems)('with triggerElement as %s', (triggerItem) => { expect(findButton().text()).toBe(displayText); }); - it('uses the default qa selector value', () => { + it('uses the default testid value', () => { createComponent(); - expect(findButton().attributes('data-qa-selector')).toBe(TRIGGER_DEFAULT_QA_SELECTOR); + expect(findButton().attributes('data-testid')).toBe(TRIGGER_DEFAULT_TESTID); }); - it('sets the qa selector value', () => { - createComponent({ qaSelector: '_qaSelector_' }); + it('sets the testid value', () => { + createComponent({ testId: '_testId_' }); - expect(findButton().attributes('data-qa-selector')).toBe('_qaSelector_'); + expect(findButton().attributes('data-testid')).toBe('_testId_'); }); }); diff --git a/spec/frontend/invite_members/components/invite_modal_base_spec.js b/spec/frontend/invite_members/components/invite_modal_base_spec.js index 26e7d98b8f4..a858f964c18 100644 --- a/spec/frontend/invite_members/components/invite_modal_base_spec.js +++ b/spec/frontend/invite_members/components/invite_modal_base_spec.js @@ -95,7 +95,7 @@ describe('InviteModalBase', () => { const actionButton = findActionButton(); expect(actionButton.text()).toBe(INVITE_BUTTON_TEXT); - expect(actionButton.attributes('data-qa-selector')).toBe('invite_button'); + expect(actionButton.attributes('data-testid')).toBe('invite-modal-submit'); expect(actionButton.props()).toMatchObject({ variant: 'confirm', diff --git a/spec/frontend/notes/components/comment_form_spec.js b/spec/frontend/notes/components/comment_form_spec.js index 1309fd79c14..8f761476c7c 100644 --- a/spec/frontend/notes/components/comment_form_spec.js +++ b/spec/frontend/notes/components/comment_form_spec.js @@ -9,7 +9,7 @@ import waitForPromises from 'helpers/wait_for_promises'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import { useLocalStorageSpy } from 'helpers/local_storage_helper'; import batchComments from '~/batch_comments/stores/modules/batch_comments'; -import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests'; +import { fetchUserCounts } from '~/super_sidebar/user_counts_fetch'; import { createAlert } from '~/alert'; import { STATUS_CLOSED, STATUS_OPEN } from '~/issues/constants'; import axios from '~/lib/utils/axios_utils'; @@ -26,7 +26,7 @@ import { mockTracking } from 'helpers/tracking_helper'; import { loggedOutnoteableData, notesDataMock, userDataMock, noteableDataMock } from '../mock_data'; jest.mock('autosize'); -jest.mock('~/commons/nav/user_merge_requests'); +jest.mock('~/super_sidebar/user_counts_fetch'); jest.mock('~/alert'); Vue.use(Vuex); @@ -586,7 +586,7 @@ describe('issue_comment_form component', () => { await nextTick(); - expect(refreshUserMergeRequestCounts).toHaveBeenCalled(); + expect(fetchUserCounts).toHaveBeenCalled(); }); }); }); diff --git a/spec/frontend/sidebar/components/reviewers/sidebar_reviewers_spec.js b/spec/frontend/sidebar/components/reviewers/sidebar_reviewers_spec.js index a221d28704b..ae31e60254f 100644 --- a/spec/frontend/sidebar/components/reviewers/sidebar_reviewers_spec.js +++ b/spec/frontend/sidebar/components/reviewers/sidebar_reviewers_spec.js @@ -1,5 +1,5 @@ import { shallowMount } from '@vue/test-utils'; -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; import axios from 'axios'; import AxiosMockAdapter from 'axios-mock-adapter'; import VueApollo from 'vue-apollo'; @@ -8,8 +8,11 @@ import SidebarReviewers from '~/sidebar/components/reviewers/sidebar_reviewers.v import SidebarService from '~/sidebar/services/sidebar_service'; import SidebarMediator from '~/sidebar/sidebar_mediator'; import SidebarStore from '~/sidebar/stores/sidebar_store'; +import { fetchUserCounts } from '~/super_sidebar/user_counts_fetch'; import Mock from '../../mock_data'; +jest.mock('~/super_sidebar/user_counts_fetch'); + Vue.use(VueApollo); describe('sidebar reviewers', () => { @@ -39,7 +42,7 @@ describe('sidebar reviewers', () => { axiosMock = new AxiosMockAdapter(axios); mediator = new SidebarMediator(Mock.mediator); - jest.spyOn(mediator, 'saveReviewers'); + jest.spyOn(mediator, 'saveReviewers').mockResolvedValue({}); jest.spyOn(mediator, 'addSelfReview'); }); @@ -60,6 +63,17 @@ describe('sidebar reviewers', () => { expect(mediator.saveReviewers).toHaveBeenCalled(); }); + it('re-fetches user counts after saving reviewers', async () => { + createComponent(); + + expect(fetchUserCounts).not.toHaveBeenCalled(); + + wrapper.vm.saveReviewers(); + await nextTick(); + + expect(fetchUserCounts).toHaveBeenCalled(); + }); + it('calls the mediator when "reviewBySelf" method is called', () => { createComponent(); diff --git a/spec/frontend/sidebar/sidebar_mediator_spec.js b/spec/frontend/sidebar/sidebar_mediator_spec.js index 9c12088216b..4dc285fc3c8 100644 --- a/spec/frontend/sidebar/sidebar_mediator_spec.js +++ b/spec/frontend/sidebar/sidebar_mediator_spec.js @@ -9,7 +9,6 @@ import Mock from './mock_data'; jest.mock('~/alert'); jest.mock('~/vue_shared/plugins/global_toast'); -jest.mock('~/commons/nav/user_merge_requests'); describe('Sidebar mediator', () => { const { mediator: mediatorMockData } = Mock; diff --git a/spec/frontend/super_sidebar/user_counts_manager_spec.js b/spec/frontend/super_sidebar/user_counts_manager_spec.js index b5074620195..3b2ee5b0991 100644 --- a/spec/frontend/super_sidebar/user_counts_manager_spec.js +++ b/spec/frontend/super_sidebar/user_counts_manager_spec.js @@ -6,6 +6,7 @@ import { userCounts, destroyUserCountsManager, } from '~/super_sidebar/user_counts_manager'; +import { fetchUserCounts } from '~/super_sidebar/user_counts_fetch'; jest.mock('~/api'); @@ -118,15 +119,30 @@ describe('User Merge Requests', () => { createUserCountsManager(); }); - it('fetches counts from API, stores and rebroadcasts them', async () => { - expect(userCounts).toMatchObject(userCountDefaults); + describe('manually created event', () => { + it('fetches counts from API, stores and rebroadcasts them', async () => { + expect(userCounts).toMatchObject(userCountDefaults); + + document.dispatchEvent(new CustomEvent('userCounts:fetch')); + await waitForPromises(); + + expect(UserApi.getUserCounts).toHaveBeenCalled(); + expect(userCounts).toMatchObject(userCountUpdate); + expect(channelMock.postMessage).toHaveBeenLastCalledWith(userCounts); + }); + }); + + describe('fetchUserCounts helper', () => { + it('fetches counts from API, stores and rebroadcasts them', async () => { + expect(userCounts).toMatchObject(userCountDefaults); - document.dispatchEvent(new CustomEvent('userCounts:fetch')); - await waitForPromises(); + fetchUserCounts(); + await waitForPromises(); - expect(UserApi.getUserCounts).toHaveBeenCalled(); - expect(userCounts).toMatchObject(userCountUpdate); - expect(channelMock.postMessage).toHaveBeenLastCalledWith(userCounts); + expect(UserApi.getUserCounts).toHaveBeenCalled(); + expect(userCounts).toMatchObject(userCountUpdate); + expect(channelMock.postMessage).toHaveBeenLastCalledWith(userCounts); + }); }); }); diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_merging_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_merging_spec.js index 85acd5f9a9e..328c0134368 100644 --- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_merging_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_merging_spec.js @@ -1,8 +1,12 @@ import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import simplePoll from '~/lib/utils/simple_poll'; import MrWidgetMerging from '~/vue_merge_request_widget/components/states/mr_widget_merging.vue'; import BoldText from '~/vue_merge_request_widget/components/bold_text.vue'; +import { STATUS_MERGED } from '~/issues/constants'; +import { fetchUserCounts } from '~/super_sidebar/user_counts_fetch'; +jest.mock('~/super_sidebar/user_counts_fetch'); jest.mock('~/lib/utils/simple_poll', () => jest.fn().mockImplementation(jest.requireActual('~/lib/utils/simple_poll').default), ); @@ -13,7 +17,7 @@ describe('MRWidgetMerging', () => { const pollMock = jest.fn().mockResolvedValue(); const GlEmoji = { template: '<img />' }; - beforeEach(() => { + const createComponent = () => { wrapper = shallowMount(MrWidgetMerging, { propsData: { mr: { @@ -29,14 +33,18 @@ describe('MRWidgetMerging', () => { GlEmoji, }, }); - }); + }; it('renders information about merge request being merged', () => { + createComponent(); + const message = wrapper.findComponent(BoldText).props('message'); expect(message).toContain('Merging!'); }); describe('initiateMergePolling', () => { + beforeEach(createComponent); + it('should call simplePoll', () => { expect(simplePoll).toHaveBeenCalledWith(expect.any(Function), { timeout: 0 }); }); @@ -45,4 +53,15 @@ describe('MRWidgetMerging', () => { expect(pollMock).toHaveBeenCalled(); }); }); + + describe('on successful merge', () => { + it('should re-fetch user counts', async () => { + pollMock.mockResolvedValueOnce({ data: { state: STATUS_MERGED } }); + createComponent(); + + await nextTick(); + + expect(fetchUserCounts).toHaveBeenCalled(); + }); + }); }); diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js index 9239807ae71..dbfd3cec76c 100644 --- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js @@ -19,9 +19,6 @@ import eventHub from '~/vue_merge_request_widget/event_hub'; jest.mock('~/lib/utils/simple_poll', () => jest.fn().mockImplementation(jest.requireActual('~/lib/utils/simple_poll').default), ); -jest.mock('~/commons/nav/user_merge_requests', () => ({ - refreshUserMergeRequestCounts: jest.fn(), -})); const commitMessage = readyToMergeResponse.data.project.mergeRequest.defaultMergeCommitMessage; const squashCommitMessage = diff --git a/spec/graphql/types/work_items/widget_definition_interface_spec.rb b/spec/graphql/types/work_items/widget_definition_interface_spec.rb new file mode 100644 index 00000000000..6f1fd0cc572 --- /dev/null +++ b/spec/graphql/types/work_items/widget_definition_interface_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Types::WorkItems::WidgetDefinitionInterface, feature_category: :team_planning do + it 'exposes the expected fields' do + expected_fields = %i[ + type + ] + + expect(described_class).to have_graphql_fields(*expected_fields) + end + + describe '.resolve_type' do + subject { described_class.resolve_type(object, {}) } + + context 'for assignees widget' do + let(:object) { WorkItems::Widgets::Assignees } + + it { is_expected.to eq(Types::WorkItems::WidgetDefinitions::AssigneesType) } + end + + context 'for other widgets' do + let(:object) { WorkItems::Widgets::Description } + + it { is_expected.to eq(Types::WorkItems::WidgetDefinitions::GenericType) } + end + end +end diff --git a/spec/graphql/types/work_items/widget_definitions/assignees_type_spec.rb b/spec/graphql/types/work_items/widget_definitions/assignees_type_spec.rb new file mode 100644 index 00000000000..d6179e92c02 --- /dev/null +++ b/spec/graphql/types/work_items/widget_definitions/assignees_type_spec.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Types::WorkItems::WidgetDefinitions::AssigneesType, feature_category: :team_planning do + it 'exposes the expected fields' do + expected_fields = %i[type can_invite_members] + + expect(described_class).to have_graphql_fields(*expected_fields) + end +end diff --git a/spec/graphql/types/work_items/widget_definitions/generic_type_spec.rb b/spec/graphql/types/work_items/widget_definitions/generic_type_spec.rb new file mode 100644 index 00000000000..19e962e71fd --- /dev/null +++ b/spec/graphql/types/work_items/widget_definitions/generic_type_spec.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Types::WorkItems::WidgetDefinitions::GenericType, feature_category: :team_planning do + it 'exposes the expected fields' do + expected_fields = %i[type] + + expect(described_class).to have_graphql_fields(*expected_fields) + end +end diff --git a/spec/lib/backup/repositories_spec.rb b/spec/lib/backup/repositories_spec.rb index ad5fb8ea84e..139c178afde 100644 --- a/spec/lib/backup/repositories_spec.rb +++ b/spec/lib/backup/repositories_spec.rb @@ -249,36 +249,6 @@ RSpec.describe Backup::Repositories, feature_category: :backup_restore do end end - context 'cleanup snippets' do - before do - error_response = ServiceResponse.error(message: "Repository has more than one branch") - allow(Snippets::RepositoryValidationService).to receive_message_chain(:new, :execute).and_return(error_response) - end - - it 'shows the appropriate error' do - subject.restore(destination, backup_id) - - expect(progress).to have_received(:puts).with("Snippet #{personal_snippet.full_path} can't be restored: Repository has more than one branch") - expect(progress).to have_received(:puts).with("Snippet #{project_snippet.full_path} can't be restored: Repository has more than one branch") - end - - it 'removes the snippets from the DB' do - expect { subject.restore(destination, backup_id) }.to change(PersonalSnippet, :count).by(-1) - .and change(ProjectSnippet, :count).by(-1) - .and change(SnippetRepository, :count).by(-2) - end - - it 'removes the repository from disk' do - gitlab_shell = Gitlab::Shell.new - shard_name = personal_snippet.repository.shard - path = personal_snippet.disk_path + '.git' - - subject.restore(destination, backup_id) - - expect(gitlab_shell.repository_exists?(shard_name, path)).to eq false - end - end - context 'storages' do let(:storages) { %w[default] } diff --git a/spec/lib/gitlab/patch/sidekiq_scheduled_enq_spec.rb b/spec/lib/gitlab/patch/sidekiq_scheduled_enq_spec.rb deleted file mode 100644 index cd3718f5dcc..00000000000 --- a/spec/lib/gitlab/patch/sidekiq_scheduled_enq_spec.rb +++ /dev/null @@ -1,73 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Patch::SidekiqScheduledEnq, :clean_gitlab_redis_queues, feature_category: :scalability do - describe '#enqueue_jobs' do - let_it_be(:payload) { {} } - - before do - allow(Sidekiq).to receive(:load_json).and_return(payload) - - # stub data in both namespaces - Gitlab::Redis::Queues.with { |c| c.zadd('resque:gitlab:schedule', 100, 'dummy') } - Gitlab::Redis::Queues.with { |c| c.zadd('schedule', 100, 'dummy') } - end - - subject { Sidekiq::Scheduled::Enq.new.enqueue_jobs } - - it 'polls both namespaces by default' do - expect(Sidekiq::Client).to receive(:push).with(payload).twice - - subject - - Sidekiq.redis do |conn| - expect(conn.zcard('schedule')).to eq(0) - end - - Gitlab::Redis::Queues.with do |conn| - expect(conn.zcard('resque:gitlab:schedule')).to eq(0) - end - end - - context 'when SIDEKIQ_ENABLE_DUAL_NAMESPACE_POLLING is disabled' do - before do - stub_env('SIDEKIQ_ENABLE_DUAL_NAMESPACE_POLLING', 'false') - end - - it 'polls via Sidekiq.redis only' do - expect(Sidekiq::Client).to receive(:push).with(payload).once - - subject - - Sidekiq.redis do |conn| - expect(conn.zcard('schedule')).to eq(0) - end - - Gitlab::Redis::Queues.with do |conn| - expect(conn.zcard('resque:gitlab:schedule')).to eq(1) - end - end - end - - context 'when SIDEKIQ_ENABLE_DUAL_NAMESPACE_POLLING is enabled' do - before do - stub_env('SIDEKIQ_ENABLE_DUAL_NAMESPACE_POLLING', 'true') - end - - it 'polls both sets' do - expect(Sidekiq::Client).to receive(:push).with(payload).twice - - subject - - Sidekiq.redis do |conn| - expect(conn.zcard('schedule')).to eq(0) - end - - Gitlab::Redis::Queues.with do |conn| - expect(conn.zcard('resque:gitlab:schedule')).to eq(0) - end - end - end - end -end diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index 06492056da2..72c68c83014 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -343,7 +343,6 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures, feature_category: :servic create(:issue, project: project, author: Users::Internal.support_bot) create(:note, project: project, noteable: issue, author: user) create(:todo, project: project, target: issue, author: user) - create(:jira_integration, :jira_cloud_service, active: true, project: create(:project, :jira_dvcs_cloud, creator: user)) create(:jira_integration, active: true, project: create(:project, :jira_dvcs_server, creator: user)) end @@ -354,7 +353,6 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures, feature_category: :servic service_desk_enabled_projects: 2, service_desk_issues: 2, projects_jira_active: 2, - projects_jira_dvcs_cloud_active: 2, projects_jira_dvcs_server_active: 2 ) expect(described_class.usage_activity_by_stage_plan(described_class.monthly_time_range_db_params)).to include( @@ -364,7 +362,6 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures, feature_category: :servic service_desk_enabled_projects: 1, service_desk_issues: 1, projects_jira_active: 1, - projects_jira_dvcs_cloud_active: 1, projects_jira_dvcs_server_active: 1 ) end diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb index a808cb1c823..35ce678e3a6 100644 --- a/spec/models/ability_spec.rb +++ b/spec/models/ability_spec.rb @@ -483,4 +483,38 @@ RSpec.describe Ability do end end end + + describe '.allowed?' do + context 'when used with :read_namespace' do + subject(:allowed?) { described_class.allowed?(nil, :read_namespace) } + + before do + allow(Gitlab::AppLogger).to receive(:info) + end + + it 'logs the usage', :aggregate_failures do + allowed? + + expect(Gitlab::AppLogger).to have_received(:info) do |args| + expect(args[:message]).to eq('Ability is in use') + expect(args[:ability]).to eq(:read_namespace) + expect(args[:caller_locations].first) + .to include('/spec/models/ability_spec.rb:489:in `block (4 levels) in <top (required)>') + expect(args[:caller_locations].length).to eq(5) + end + end + + context 'when :log_read_namespace_usages feature flag is disabled' do + before do + stub_feature_flags(log_read_namespace_usages: false) + end + + it 'does not log the usage' do + allowed? + + expect(Gitlab::AppLogger).not_to have_received(:info) + end + end + end + end end diff --git a/spec/models/bulk_imports/tracker_spec.rb b/spec/models/bulk_imports/tracker_spec.rb index edd9adfa5f6..25cd5489a9f 100644 --- a/spec/models/bulk_imports/tracker_spec.rb +++ b/spec/models/bulk_imports/tracker_spec.rb @@ -83,5 +83,31 @@ RSpec.describe BulkImports::Tracker, type: :model, feature_category: :importers "'InexistingPipeline' is not a valid BulkImport Pipeline" ) end + + context 'when using delegation methods' do + context 'with group pipelines' do + let(:entity) { create(:bulk_import_entity) } + + it 'does not raise' do + entity.pipelines.each do |pipeline| + tracker = create(:bulk_import_tracker, entity: entity, pipeline_name: pipeline[:pipeline]) + expect { tracker.abort_on_failure? }.not_to raise_error + expect { tracker.file_extraction_pipeline? }.not_to raise_error + end + end + end + + context 'with project pipelines' do + let(:entity) { create(:bulk_import_entity, :project_entity) } + + it 'does not raise' do + entity.pipelines.each do |pipeline| + tracker = create(:bulk_import_tracker, entity: entity, pipeline_name: pipeline[:pipeline]) + expect { tracker.abort_on_failure? }.not_to raise_error + expect { tracker.file_extraction_pipeline? }.not_to raise_error + end + end + end + end end end diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb index ee48e8cac6c..cb2c38c15e0 100644 --- a/spec/models/deployment_spec.rb +++ b/spec/models/deployment_spec.rb @@ -1270,10 +1270,15 @@ RSpec.describe Deployment, feature_category: :continuous_delivery do shared_examples_for 'gracefully handling error' do it 'tracks an exception' do - expect(Gitlab::ErrorTracking).to receive(:track_exception).with( - instance_of(described_class::StatusSyncError), - deployment_id: deployment.id, - job_id: job.id) + expect(Gitlab::ErrorTracking).to( + receive(:track_exception).with( + instance_of(described_class::StatusSyncError), + deployment_id: deployment.id, + job_id: job.id + ) do |error| + expect(error.backtrace).to be_present + end + ) is_expected.to eq(false) diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 3ea5f6ea0ae..52c341a754e 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -2176,19 +2176,9 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr end end - describe '.with_jira_dvcs_cloud' do - it 'returns the correct project' do - jira_dvcs_cloud_project = create(:project, :jira_dvcs_cloud) - create(:project, :jira_dvcs_server) - - expect(described_class.with_jira_dvcs_cloud).to contain_exactly(jira_dvcs_cloud_project) - end - end - describe '.with_jira_dvcs_server' do it 'returns the correct project' do jira_dvcs_server_project = create(:project, :jira_dvcs_server) - create(:project, :jira_dvcs_cloud) expect(described_class.with_jira_dvcs_server).to contain_exactly(jira_dvcs_server_project) end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 606c4ea05b9..974848b3967 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -4011,4 +4011,59 @@ RSpec.describe Repository, feature_category: :source_code_management do it { expect { file_attributes }.to raise_error(ArgumentError) } end end + + describe '#filter_generated_files' do + let(:project) do + create(:project, :custom_repo, files: { + '.gitattributes' => gitattr_content, + 'file1.txt' => 'first file', + 'file2.txt' => 'second file' + }) + end + + let(:rev) { 'master' } + let(:paths) { ['file1.txt', 'file2.txt'] } + + subject(:generated_files) { repository.filter_generated_files(rev, paths) } + + context 'when the linguist-generated attribute is used' do + let(:gitattr_content) { "*.txt text\nfile1.txt linguist-generated\n" } + + it 'returns generated files only' do + expect(generated_files).to contain_exactly('file2.txt') + end + end + + context 'when the gitlab-generated attribute is used' do + let(:gitattr_content) { "*.txt text\nfile1.txt gitlab-generated\n" } + + it 'returns generated files only' do + expect(generated_files).to contain_exactly('file2.txt') + end + end + + context 'when both linguist-generated and gitlab-generated attribute are used' do + let(:gitattr_content) { "*.txt text\nfile1.txt linguist-generated gitlab-generated\n" } + + it 'returns generated files only' do + expect(generated_files).to contain_exactly('file2.txt') + end + end + + context 'when no generated overrides are used' do + let(:gitattr_content) { "*.txt text\n" } + + it 'returns the original paths' do + expect(generated_files).to eq paths + end + end + + context 'when the given files are generated' do + let(:gitattr_content) { "*.txt gitlab-generated\n" } + + it 'returns an empty array' do + expect(generated_files).to eq [] + end + end + end end diff --git a/spec/models/tree_spec.rb b/spec/models/tree_spec.rb index 20d786f311f..302cd9e9f10 100644 --- a/spec/models/tree_spec.rb +++ b/spec/models/tree_spec.rb @@ -2,69 +2,58 @@ require 'spec_helper' -RSpec.describe Tree do - let_it_be(:repository) { create(:project, :repository).repository } - - let(:sha) { repository.root_ref } - +RSpec.describe Tree, feature_category: :source_code_management do subject(:tree) { described_class.new(repository, '54fcc214') } - describe '#readme' do - before do - stub_const('FakeBlob', Class.new) - FakeBlob.class_eval do - attr_reader :name + let_it_be(:repository) { create(:project, :repository).repository } - def initialize(name) - @name = name - end + describe '#readme' do + subject { tree.readme } - def readme? - name =~ /^readme/i - end - end + before do + allow(tree).to receive(:blobs).and_return(files) end - it 'returns nil when repository does not contains a README file' do - files = [FakeBlob.new('file'), FakeBlob.new('license'), FakeBlob.new('copying')] - expect(subject).to receive(:blobs).and_return(files) + context 'when repository does not contains a README file' do + let(:files) { [fake_blob('file'), fake_blob('license'), fake_blob('copying')] } - expect(subject.readme).to eq nil + it { is_expected.to be_nil } end - it 'returns nil when repository does not contains a previewable README file' do - files = [FakeBlob.new('file'), FakeBlob.new('README.pages'), FakeBlob.new('README.png')] - expect(subject).to receive(:blobs).and_return(files) + context 'when repository does not contains a previewable README file' do + let(:files) { [fake_blob('file'), fake_blob('README.pages'), fake_blob('README.png')] } - expect(subject.readme).to eq nil + it { is_expected.to be_nil } end - it 'returns README when repository contains a previewable README file' do - files = [FakeBlob.new('README.png'), FakeBlob.new('README'), FakeBlob.new('file')] - expect(subject).to receive(:blobs).and_return(files) + context 'when repository contains a previewable README file' do + let(:files) { [fake_blob('README.png'), fake_blob('README'), fake_blob('file')] } - expect(subject.readme.name).to eq 'README' + it { is_expected.to have_attributes(name: 'README') } end - it 'returns first previewable README when repository contains more than one' do - files = [FakeBlob.new('file'), FakeBlob.new('README.md'), FakeBlob.new('README.asciidoc')] - expect(subject).to receive(:blobs).and_return(files) + context 'when repository contains more than one README file' do + let(:files) { [fake_blob('file'), fake_blob('README.md'), fake_blob('README.asciidoc')] } - expect(subject.readme.name).to eq 'README.md' - end + it 'returns first previewable README' do + is_expected.to have_attributes(name: 'README.md') + end - it 'returns first plain text README when repository contains more than one' do - files = [FakeBlob.new('file'), FakeBlob.new('README'), FakeBlob.new('README.txt')] - expect(subject).to receive(:blobs).and_return(files) + context 'when only plain-text READMEs' do + let(:files) { [fake_blob('file'), fake_blob('README'), fake_blob('README.txt')] } - expect(subject.readme.name).to eq 'README' + it 'returns first plain text README' do + is_expected.to have_attributes(name: 'README') + end + end end - it 'prioritizes previewable README file over one in plain text' do - files = [FakeBlob.new('file'), FakeBlob.new('README'), FakeBlob.new('README.md')] - expect(subject).to receive(:blobs).and_return(files) + context 'when the repository has a previewable and plain text READMEs' do + let(:files) { [fake_blob('file'), fake_blob('README'), fake_blob('README.md')] } - expect(subject.readme.name).to eq 'README.md' + it 'prefers previewable README file' do + is_expected.to have_attributes(name: 'README.md') + end end end @@ -73,4 +62,10 @@ RSpec.describe Tree do it { is_expected.to be_an_instance_of(Gitaly::PaginationCursor) } end + + private + + def fake_blob(name) + instance_double(Gitlab::Git::Blob, name: name) + end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index fe229ce836f..5a73be94f67 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -6381,6 +6381,49 @@ RSpec.describe User, feature_category: :user_profile do end end end + + context 'with multiple versions of terms' do + shared_examples 'terms acceptance' do + let(:another_term) { create :term } + let(:required_terms_are_accepted) { !required_terms_not_accepted } + + context 'when the latest term is not accepted' do + before do + accept_terms(user) + another_term + end + + it { expect(required_terms_are_accepted).to be result_for_latest_not_accepted } + end + + context 'when the latest term is accepted' do + before do + another_term + accept_terms(user) + end + + it { expect(required_terms_are_accepted).to be result_for_latest_accepted } + end + end + + context 'when enforce_acceptance_of_changed_terms is enabled' do + let(:result_for_latest_not_accepted) { false } + let(:result_for_latest_accepted) { true } + + include_examples 'terms acceptance' + end + + context 'when enforce_acceptance_of_changed_terms is disabled' do + let(:result_for_latest_not_accepted) { true } + let(:result_for_latest_accepted) { true } + + before do + stub_feature_flags(enforce_acceptance_of_changed_terms: false) + end + + include_examples 'terms acceptance' + end + end end end diff --git a/spec/models/work_items/widgets/assignees_spec.rb b/spec/models/work_items/widgets/assignees_spec.rb index 19c17658ce4..b44246855ce 100644 --- a/spec/models/work_items/widgets/assignees_spec.rb +++ b/spec/models/work_items/widgets/assignees_spec.rb @@ -17,6 +17,32 @@ RSpec.describe WorkItems::Widgets::Assignees do it { is_expected.to include(:assignee_ids) } end + describe '.can_invite_members?' do + let(:user) { build_stubbed(:user) } + + subject(:execute) { described_class.can_invite_members?(user, resource_parent) } + + context 'when resource_parent is a project' do + let(:resource_parent) { build_stubbed(:project) } + + it 'checks the ability with the correct permission' do + expect(user).to receive(:can?).with(:admin_project_member, resource_parent) + + execute + end + end + + context 'when resource_parent is a group' do + let(:resource_parent) { build_stubbed(:group) } + + it 'checks the ability with the correct permission' do + expect(user).to receive(:can?).with(:admin_group_member, resource_parent) + + execute + end + end + end + describe '#type' do subject { described_class.new(work_item).type } diff --git a/spec/policies/ci/runner_manager_policy_spec.rb b/spec/policies/ci/runner_manager_policy_spec.rb index d7004033ceb..d4f52879714 100644 --- a/spec/policies/ci/runner_manager_policy_spec.rb +++ b/spec/policies/ci/runner_manager_policy_spec.rb @@ -129,7 +129,29 @@ RSpec.describe Ci::RunnerManagerPolicy, feature_category: :runner_fleet do context 'with project runner' do let(:runner) { project_runner } - it { expect_disallowed :read_runner_manager } + it { expect_allowed :read_runner_manager } + + context 'when user is not developer in parent group' do + let_it_be(:developers_group_developer) { create(:user) } + let_it_be_with_reload(:developers_group) { create(:group, name: 'developers', path: 'developers') } + + let(:user) { developers_group_developer } + + before_all do + create(:project_group_link, :developer, group: developers_group, project: project) + developers_group.add_reporter(developers_group_developer) + end + + it { expect_disallowed :read_runner_manager } + + context 'when user is developer in a group invited to project as developer' do + before_all do + developers_group.add_developer(developers_group_developer) + end + + it { expect_allowed :read_runner_manager } + end + end end end diff --git a/spec/policies/ci/runner_policy_spec.rb b/spec/policies/ci/runner_policy_spec.rb index e0a9e3c2870..007669b8f52 100644 --- a/spec/policies/ci/runner_policy_spec.rb +++ b/spec/policies/ci/runner_policy_spec.rb @@ -127,7 +127,29 @@ RSpec.describe Ci::RunnerPolicy, feature_category: :runner do context 'with project runner' do let(:runner) { project_runner } - it { expect_disallowed :read_runner } + it { expect_allowed :read_runner } + + context 'when user is not developer in parent group' do + let_it_be(:developers_group_developer) { create(:user) } + let_it_be_with_reload(:developers_group) { create(:group, name: 'developers', path: 'developers') } + + let(:user) { developers_group_developer } + + before_all do + create(:project_group_link, :developer, group: developers_group, project: project) + developers_group.add_reporter(developers_group_developer) + end + + it { expect_disallowed :read_runner } + + context 'when user is developer in a group invited to project as developer' do + before_all do + developers_group.add_developer(developers_group_developer) + end + + it { expect_allowed :read_runner } + end + end end end diff --git a/spec/requests/api/graphql/group/work_item_types_spec.rb b/spec/requests/api/graphql/group/work_item_types_spec.rb index 791c0fb9524..fbebcdad389 100644 --- a/spec/requests/api/graphql/group/work_item_types_spec.rb +++ b/spec/requests/api/graphql/group/work_item_types_spec.rb @@ -5,56 +5,19 @@ require 'spec_helper' RSpec.describe 'getting a list of work item types for a group', feature_category: :team_planning do include GraphqlHelpers - let_it_be(:developer) { create(:user) } let_it_be(:group) { create(:group, :private) } + let_it_be(:developer) { create(:user).tap { |u| group.add_developer(u) } } - before_all do - group.add_developer(developer) - end - - let(:current_user) { developer } - - let(:fields) do - <<~GRAPHQL - workItemTypes{ - nodes { id name iconName } - } - GRAPHQL - end - - let(:query) do - graphql_query_for( - 'group', - { 'fullPath' => group.full_path }, - fields - ) - end - - context 'when user has access to the group' do - before do - post_graphql(query, current_user: current_user) - end + it_behaves_like 'graphql work item type list request spec' do + let(:current_user) { developer } + let(:parent_key) { :group } - it_behaves_like 'a working graphql query' - - it 'returns all default work item types' do - expect(graphql_data.dig('group', 'workItemTypes', 'nodes')).to match_array( - WorkItems::Type.default.map do |type| - hash_including('id' => type.to_global_id.to_s, 'name' => type.name, 'iconName' => type.icon_name) - end + let(:query) do + graphql_query_for( + 'group', + { 'fullPath' => group.full_path }, + query_nodes('WorkItemTypes', work_item_type_fields) ) end end - - context "when user doesn't have access to the group" do - let(:current_user) { create(:user) } - - before do - post_graphql(query, current_user: current_user) - end - - it 'does not return the group' do - expect(graphql_data).to eq('group' => nil) - end - end end diff --git a/spec/requests/api/graphql/project/work_item_types_spec.rb b/spec/requests/api/graphql/project/work_item_types_spec.rb index c31a260c4b8..086db983760 100644 --- a/spec/requests/api/graphql/project/work_item_types_spec.rb +++ b/spec/requests/api/graphql/project/work_item_types_spec.rb @@ -5,56 +5,19 @@ require 'spec_helper' RSpec.describe 'getting a list of work item types for a project', feature_category: :team_planning do include GraphqlHelpers - let_it_be(:developer) { create(:user) } let_it_be(:project) { create(:project) } + let_it_be(:developer) { create(:user).tap { |u| project.add_developer(u) } } - before_all do - project.add_developer(developer) - end - - let(:current_user) { developer } - - let(:fields) do - <<~GRAPHQL - workItemTypes{ - nodes { id name iconName } - } - GRAPHQL - end - - let(:query) do - graphql_query_for( - 'project', - { 'fullPath' => project.full_path }, - fields - ) - end - - context 'when user has access to the project' do - before do - post_graphql(query, current_user: current_user) - end + it_behaves_like 'graphql work item type list request spec' do + let(:current_user) { developer } + let(:parent_key) { :project } - it_behaves_like 'a working graphql query' - - it 'returns all default work item types' do - expect(graphql_data.dig('project', 'workItemTypes', 'nodes')).to match_array( - WorkItems::Type.default.map do |type| - hash_including('id' => type.to_global_id.to_s, 'name' => type.name, 'iconName' => type.icon_name) - end + let(:query) do + graphql_query_for( + 'project', + { 'fullPath' => project.full_path }, + query_nodes('WorkItemTypes', work_item_type_fields) ) end end - - context "when user doesn't have access to the project" do - let(:current_user) { create(:user) } - - before do - post_graphql(query, current_user: current_user) - end - - it 'does not return the project' do - expect(graphql_data).to eq('project' => nil) - end - end end diff --git a/spec/requests/api/graphql/work_item_spec.rb b/spec/requests/api/graphql/work_item_spec.rb index 36a27abd982..fe77b7ae736 100644 --- a/spec/requests/api/graphql/work_item_spec.rb +++ b/spec/requests/api/graphql/work_item_spec.rb @@ -104,6 +104,18 @@ RSpec.describe 'Query.work_item(id)', feature_category: :team_planning do end end + context 'when querying work item type information' do + include_context 'with work item types request context' + + let(:work_item_fields) { "workItemType { #{work_item_type_fields} }" } + + it 'returns work item type information' do + expect(work_item_data['workItemType']).to match( + expected_work_item_type_response(work_item.work_item_type).first + ) + end + end + context 'when querying widgets' do describe 'description widget' do let(:work_item_fields) do diff --git a/spec/support/shared_contexts/requests/api/graphql/work_items/work_item_types_shared_context.rb b/spec/support/shared_contexts/requests/api/graphql/work_items/work_item_types_shared_context.rb new file mode 100644 index 00000000000..c3c2ddcea27 --- /dev/null +++ b/spec/support/shared_contexts/requests/api/graphql/work_items/work_item_types_shared_context.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +RSpec.shared_context 'with work item types request context' do + let(:work_item_type_fields) do + <<~GRAPHQL + id + name + iconName + widgetDefinitions { + type + ... on WorkItemWidgetDefinitionAssignees { + canInviteMembers + } + } + GRAPHQL + end + + let(:widget_attributes) do + { + assignees: { + 'canInviteMembers' => false + } + } + end + + def expected_work_item_type_response(type = nil) + base_scope = WorkItems::Type.default + base_scope = base_scope.id_in(type.id) if type + + base_scope.map do |type| + hash_including( + 'id' => type.to_global_id.to_s, + 'name' => type.name, + 'iconName' => type.icon_name, + 'widgetDefinitions' => match_array(widgets_for(type)) + ) + end + end + + def widgets_for(type) + type.widgets.map do |widget| + base_attributes = { 'type' => widget.type.to_s.upcase } + next base_attributes unless widget_attributes[widget.type] + + base_attributes.merge(widget_attributes[widget.type]) + end + end +end diff --git a/spec/support/shared_examples/requests/api/graphql/work_item_type_list_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/work_item_type_list_shared_examples.rb new file mode 100644 index 00000000000..a5315aa47a0 --- /dev/null +++ b/spec/support/shared_examples/requests/api/graphql/work_item_type_list_shared_examples.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'graphql work item type list request spec' do + include_context 'with work item types request context' + + context 'when user has access to the group' do + it_behaves_like 'a working graphql query that returns data' do + before do + post_graphql(query, current_user: current_user) + end + end + + it 'returns all default work item types' do + post_graphql(query, current_user: current_user) + + expect(graphql_data_at(parent_key, :workItemTypes, :nodes)).to match_array(expected_work_item_type_response) + end + + it 'prevents N+1 queries' do + # Destroy 2 existing types + WorkItems::Type.by_type([:issue, :task]).delete_all + + post_graphql(query, current_user: current_user) # warm-up + + control = ActiveRecord::QueryRecorder.new(skip_cached: false) { post_graphql(query, current_user: current_user) } + expect(graphql_errors).to be_blank + + # Add back the 2 deleted types + expect do + Gitlab::DatabaseImporters::WorkItems::BaseTypeImporter.upsert_types + end.to change { WorkItems::Type.count }.by(2) + + expect { post_graphql(query, current_user: current_user) }.to issue_same_number_of_queries_as(control) + expect(graphql_errors).to be_blank + end + end + + context "when user doesn't have access to the parent" do + let(:current_user) { create(:user) } + + before do + post_graphql(query, current_user: current_user) + end + + it 'does not return the parent' do + expect(graphql_data).to eq(parent_key.to_s => nil) + end + end +end diff --git a/spec/workers/bulk_imports/pipeline_worker_spec.rb b/spec/workers/bulk_imports/pipeline_worker_spec.rb index d99b3e9de73..39f4d6c4f6d 100644 --- a/spec/workers/bulk_imports/pipeline_worker_spec.rb +++ b/spec/workers/bulk_imports/pipeline_worker_spec.rb @@ -16,12 +16,16 @@ RSpec.describe BulkImports::PipelineWorker, feature_category: :importers do def self.file_extraction_pipeline? false end + + def self.abort_on_failure? + false + end end end let_it_be(:bulk_import) { create(:bulk_import) } let_it_be(:config) { create(:bulk_import_configuration, bulk_import: bulk_import) } - let_it_be(:entity) { create(:bulk_import_entity, bulk_import: bulk_import) } + let_it_be_with_reload(:entity) { create(:bulk_import_entity, bulk_import: bulk_import) } let(:pipeline_tracker) do create( @@ -156,6 +160,21 @@ RSpec.describe BulkImports::PipelineWorker, feature_category: :importers do expect(pipeline_tracker.status_name).to eq(:failed) expect(pipeline_tracker.jid).to eq('jid') + expect(entity.reload.status_name).to eq(:created) + end + + context 'when pipeline has abort_on_failure' do + before do + allow(pipeline_class).to receive(:abort_on_failure?).and_return(true) + end + + it 'marks entity as failed' do + job = { 'args' => [pipeline_tracker.id, pipeline_tracker.stage, entity.id] } + + described_class.sidekiq_retries_exhausted_block.call(job, StandardError.new('Error!')) + + expect(entity.reload.status_name).to eq(:failed) + end end end @@ -266,6 +285,10 @@ RSpec.describe BulkImports::PipelineWorker, feature_category: :importers do describe '#perform' do context 'when entity is failed' do + before do + entity.update!(status: -1) + end + it 'marks tracker as skipped and logs the skip' do pipeline_tracker = create( :bulk_import_tracker, @@ -274,8 +297,6 @@ RSpec.describe BulkImports::PipelineWorker, feature_category: :importers do status_event: 'enqueue' ) - entity.update!(status: -1) - expect_next_instance_of(BulkImports::Logger) do |logger| allow(logger).to receive(:info) diff --git a/spec/workers/concerns/gitlab/github_import/stage_methods_spec.rb b/spec/workers/concerns/gitlab/github_import/stage_methods_spec.rb index fa782967441..07f35c9e5b7 100644 --- a/spec/workers/concerns/gitlab/github_import/stage_methods_spec.rb +++ b/spec/workers/concerns/gitlab/github_import/stage_methods_spec.rb @@ -198,17 +198,5 @@ RSpec.describe Gitlab::GithubImport::StageMethods, feature_category: :importers is_expected.to include('max_retries_after_interruption' => 20) end - - context 'when the flag is disabled' do - before do - stub_feature_flags(github_importer_raise_max_interruptions: false) - end - - it 'does not set `max_retries_after_interruption`' do - worker.class.resumes_work_when_interrupted! - - is_expected.not_to have_key('max_retries_after_interruption') - end - end end end diff --git a/vendor/gems/sidekiq-reliable-fetch/lib/sidekiq/semi_reliable_fetch.rb b/vendor/gems/sidekiq-reliable-fetch/lib/sidekiq/semi_reliable_fetch.rb index d33b5049300..91b41501374 100644 --- a/vendor/gems/sidekiq-reliable-fetch/lib/sidekiq/semi_reliable_fetch.rb +++ b/vendor/gems/sidekiq-reliable-fetch/lib/sidekiq/semi_reliable_fetch.rb @@ -10,25 +10,16 @@ module Sidekiq def initialize(options) super - @alternative_store = options[:alternative_store] - @namespace = options[:namespace] - - @queues = @queues + @queues.map { |q| "#{@namespace}:#{q}" } if @namespace - - if strictly_ordered_queues - @queues = @queues.uniq - @queues << { timeout: semi_reliable_fetch_timeout } - end + @queues = @queues.uniq end private def retrieve_unit_of_work - work = with_redis { |conn| conn.brpop(*queues_cmd) } + work = Sidekiq.redis { |conn| conn.brpop(*queues_cmd, timeout: semi_reliable_fetch_timeout) } return unless work queue, job = work - queue = queue.to_s.sub(/\A#{@namespace}:/, '') if @namespace unit_of_work = UnitOfWork.new(queue, job) Sidekiq.redis do |conn| @@ -38,21 +29,11 @@ module Sidekiq unit_of_work end - def with_redis(&blk) - if @alternative_store - @alternative_store.with(&blk) - else - Sidekiq.redis(&blk) - end - end - def queues_cmd if strictly_ordered_queues @queues else - queues = @queues.shuffle.uniq - queues << { timeout: semi_reliable_fetch_timeout } - queues + @queues.shuffle end end diff --git a/vendor/gems/sidekiq-reliable-fetch/spec/semi_reliable_fetch_spec.rb b/vendor/gems/sidekiq-reliable-fetch/spec/semi_reliable_fetch_spec.rb index d6701c7c7e1..60cd81ba913 100644 --- a/vendor/gems/sidekiq-reliable-fetch/spec/semi_reliable_fetch_spec.rb +++ b/vendor/gems/sidekiq-reliable-fetch/spec/semi_reliable_fetch_spec.rb @@ -11,40 +11,6 @@ describe Sidekiq::SemiReliableFetch do let(:options) { { queues: queues } } let(:fetcher) { described_class.new(options) } - context 'namespace config' do - let(:options) { { queues: queues, namespace: 'namespaced' } } - - before do - Sidekiq.redis do |conn| - conn.rpush('queue:stuff_to_do', 'msg1') - conn.rpush('namespaced:queue:stuff_to_do', 'msg2') - end - end - - it 'runs brpop on both namespaced and non-namespaced' do - jobs = (1..2).map { fetcher.retrieve_work&.job } - - expect(jobs).to match_array(['msg1', 'msg2']) - end - end - - context 'alternative_store config' do - let(:store) { Sidekiq::RedisConnection.create(url: REDIS_URL, size: 10) } - let(:options) { { queues: queues, alternative_store: store } } - - it 'connects using alternative store' do - Sidekiq.redis do |connection| - expect(connection).not_to receive(:brpop) - end - - store.with do |connection| - expect(connection).to receive(:brpop).with("queue:stuff_to_do", { timeout: 2 }).once.and_call_original - end - - fetcher.retrieve_work - end - end - context 'timeout config' do before do stub_env('SIDEKIQ_SEMI_RELIABLE_FETCH_TIMEOUT', timeout) diff --git a/yarn.lock b/yarn.lock index 298d39fc322..210bde0d8a7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1274,10 +1274,10 @@ resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.71.0.tgz#ff4a3cf22cd12b3c861ef2065583cc49923cf5f8" integrity sha512-aYjC9uef5Q3CDg4Zu9fh0mce4jO2LANaEgRLutoAYRXG4ymWwRmgP8SZmZyQY0B4hcZjBfUsyVykIhVnlNcRLw== -"@gitlab/ui@^68.5.0": - version "68.5.0" - resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-68.5.0.tgz#4850c4c87d924b1ae40539bf4de22a555f8e9b17" - integrity sha512-4Cr0+TTCjr7MApZOm0n9GJuWHVkFCkbldLZjsoCVVGBc6qpv2I0h+nMmbFFMrMjuVicfa9/UPXm4XIdqbw3QqA== +"@gitlab/ui@^69.0.0": + version "69.0.0" + resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-69.0.0.tgz#981979e1f4a639ef21f266c9e96b8aa6a625db87" + integrity sha512-HCWLX0/1NwQILeRQ1c0yVCcjGftX7L8Put4mmEaoCPGcX1r8aE/mdkqTzOp9vTzi3OxzaJ8FDA/GV7RaxdGP0A== dependencies: "@floating-ui/dom" "1.2.9" bootstrap-vue "2.23.1" |