diff options
100 files changed, 1165 insertions, 253 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 9fa296be455..8644659198a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -104,15 +104,20 @@ workflow: # If `$GITLAB_INTERNAL` isn't set, don't create a pipeline. - if: '$GITLAB_INTERNAL == null' when: never - # For stable, auto-deploy, and security branches, create a pipeline. - - if: '$CI_COMMIT_BRANCH =~ /^[\d-]+-stable(-ee)?$/' + # For last 3 stable branches, create a pipeline with failure notifications. + - if: '$CI_COMMIT_BRANCH =~ /^15-[6|7|8]-stable(-ee)?$/' variables: <<: *ruby2-variables NOTIFY_PIPELINE_FAILURE_CHANNEL: "releases" - PIPELINE_NAME: 'Ruby 2 $CI_COMMIT_BRANCH branch pipeline' CREATE_INCIDENT_FOR_PIPELINE_FAILURE: "true" BROKEN_BRANCH_INCIDENTS_PROJECT: "gitlab-org/release/tasks" BROKEN_BRANCH_INCIDENTS_PROJECT_TOKEN: "${BROKEN_STABLE_INCIDENTS_PROJECT_TOKEN}" + PIPELINE_NAME: 'Ruby 2 $CI_COMMIT_BRANCH branch pipeline' + # For stable, auto-deploy, and security branches, create a pipeline. + - if: '$CI_COMMIT_BRANCH =~ /^[\d-]+-stable(-ee)?$/' + variables: + <<: *ruby2-variables + PIPELINE_NAME: 'Ruby 2 $CI_COMMIT_BRANCH branch pipeline' - if: '$CI_COMMIT_BRANCH =~ /^\d+-\d+-auto-deploy-\d+$/' variables: <<: *ruby2-variables diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index ab1f709de26..d29ac054b2c 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -994a537cefce6044125fa420cd7536a6efda0618 +a41c0909051714c0d6b58f8000589f82a66804a5 diff --git a/app/assets/javascripts/issues/constants.js b/app/assets/javascripts/issues/constants.js index 10d1a2d0dbf..ba05dd731f7 100644 --- a/app/assets/javascripts/issues/constants.js +++ b/app/assets/javascripts/issues/constants.js @@ -6,6 +6,7 @@ export const STATUS_REOPENED = 'reopened'; export const TITLE_LENGTH_MAX = 255; +export const TYPE_EPIC = 'epic'; export const TYPE_ISSUE = 'issue'; export const IssuableStatusText = { diff --git a/app/assets/javascripts/issues/show/components/app.vue b/app/assets/javascripts/issues/show/components/app.vue index a0db1ddcf6f..decb559ee81 100644 --- a/app/assets/javascripts/issues/show/components/app.vue +++ b/app/assets/javascripts/issues/show/components/app.vue @@ -4,8 +4,8 @@ import Visibility from 'visibilityjs'; import { createAlert } from '~/flash'; import { IssuableStatusText, - IssuableType, STATUS_CLOSED, + TYPE_EPIC, TYPE_ISSUE, WorkspaceType, } from '~/issues/constants'; @@ -277,7 +277,7 @@ export default { return IssuableStatusText[this.issuableStatus]; }, shouldShowStickyHeader() { - return [TYPE_ISSUE, IssuableType.Epic].includes(this.issuableType); + return [TYPE_ISSUE, TYPE_EPIC].includes(this.issuableType); }, }, created() { diff --git a/app/assets/javascripts/notes/components/sidebar_subscription.vue b/app/assets/javascripts/notes/components/sidebar_subscription.vue index a33f4364097..2a0a3d5414f 100644 --- a/app/assets/javascripts/notes/components/sidebar_subscription.vue +++ b/app/assets/javascripts/notes/components/sidebar_subscription.vue @@ -1,6 +1,6 @@ <script> import { mapActions } from 'vuex'; -import { IssuableType, TYPE_ISSUE } from '~/issues/constants'; +import { TYPE_EPIC, TYPE_ISSUE } from '~/issues/constants'; import { fetchPolicies } from '~/lib/graphql'; import { confidentialityQueries } from '~/sidebar/constants'; import { defaultClient as gqlClient } from '~/graphql_shared/issuable_client'; @@ -28,7 +28,7 @@ export default { }, }, created() { - if (this.issuableType !== TYPE_ISSUE && this.issuableType !== IssuableType.Epic) { + if (this.issuableType !== TYPE_ISSUE && this.issuableType !== TYPE_EPIC) { return; } diff --git a/app/assets/javascripts/pipelines/components/parsing_utils.js b/app/assets/javascripts/pipelines/components/parsing_utils.js index 83ba984ce07..e158f8809b5 100644 --- a/app/assets/javascripts/pipelines/components/parsing_utils.js +++ b/app/assets/javascripts/pipelines/components/parsing_utils.js @@ -171,11 +171,12 @@ export const generateColumnsFromLayersListBare = ({ stages, stagesLookup }, pipe export const generateColumnsFromLayersListMemoized = memoize(generateColumnsFromLayersListBare); -// TODO: handle REST / MR values -// See https://gitlab.com/gitlab-org/gitlab/-/issues/367547 export const keepLatestDownstreamPipelines = (downstreamPipelines = []) => { - // handles GraphQL return downstreamPipelines.filter((pipeline) => { + if (pipeline.source_job) { + return !pipeline?.source_job?.retried || false; + } + return !pipeline?.sourceJob?.retried || false; }); }; diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue index ed32d643c0e..365572f194b 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue @@ -2,6 +2,7 @@ import { GlTableLite, GlTooltipDirective } from '@gitlab/ui'; import { s__, __ } from '~/locale'; import Tracking from '~/tracking'; +import { keepLatestDownstreamPipelines } from '~/pipelines/components/parsing_utils'; import PipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue'; import eventHub from '../../event_hub'; import { TRACKING_CATEGORIES } from '../../constants'; @@ -115,6 +116,10 @@ export default { eventHub.$off('openConfirmationModal', this.setModalData); }, methods: { + getDownstreamPipelines(pipeline) { + const downstream = pipeline.triggered; + return keepLatestDownstreamPipelines(downstream); + }, setModalData(data) { this.pipelineId = data.pipeline.id; this.pipeline = data.pipeline; @@ -171,7 +176,7 @@ export default { <template #cell(stages)="{ item }"> <pipeline-mini-graph - :downstream-pipelines="item.triggered" + :downstream-pipelines="getDownstreamPipelines(item)" :pipeline-path="item.path" :stages="item.details.stages" :update-dropdown="updateGraphDropdown" diff --git a/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_content.vue b/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_content.vue index 6afaee91d7a..1eeb725d5c9 100644 --- a/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_content.vue +++ b/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_content.vue @@ -1,7 +1,7 @@ <script> import { GlIcon, GlAlert, GlTooltipDirective } from '@gitlab/ui'; import { __ } from '~/locale'; -import { IssuableType, WorkspaceType } from '~/issues/constants'; +import { TYPE_EPIC, WorkspaceType } from '~/issues/constants'; import { confidentialityInfoText } from '~/vue_shared/constants'; export default { @@ -25,7 +25,7 @@ export default { computed: { confidentialBodyText() { return confidentialityInfoText( - this.issuableType === IssuableType.Epic ? WorkspaceType.group : WorkspaceType.project, + this.issuableType === TYPE_EPIC ? WorkspaceType.group : WorkspaceType.project, this.issuableType, ); }, diff --git a/app/assets/javascripts/sidebar/components/labels/labels_select_widget/labels_select_root.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/labels_select_root.vue index 60361b7de67..bf916e26a15 100644 --- a/app/assets/javascripts/sidebar/components/labels/labels_select_widget/labels_select_root.vue +++ b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/labels_select_root.vue @@ -4,7 +4,7 @@ import issuableLabelsSubscription from 'ee_else_ce/sidebar/queries/issuable_labe import { MutationOperationMode, getIdFromGraphQLId } from '~/graphql_shared/utils'; import { createAlert } from '~/flash'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; -import { IssuableType, TYPE_ISSUE } from '~/issues/constants'; +import { IssuableType, TYPE_EPIC, TYPE_ISSUE } from '~/issues/constants'; import { __ } from '~/locale'; import { issuableLabelsQueries } from '../../../constants'; @@ -269,7 +269,7 @@ export default { ...updateVariables, operationMode: MutationOperationMode.Replace, }; - case IssuableType.Epic: + case TYPE_EPIC: return { iid: currentIid, groupPath: this.fullPath, @@ -330,7 +330,7 @@ export default { labelIds: [labelId], operationMode: MutationOperationMode.Remove, }; - case IssuableType.Epic: + case TYPE_EPIC: return { iid: this.iid, removeLabelIds: [getIdFromGraphQLId(labelId)], diff --git a/app/assets/javascripts/sidebar/components/sidebar_dropdown.vue b/app/assets/javascripts/sidebar/components/sidebar_dropdown.vue index eaf87e4d4e0..d68e4974ea4 100644 --- a/app/assets/javascripts/sidebar/components/sidebar_dropdown.vue +++ b/app/assets/javascripts/sidebar/components/sidebar_dropdown.vue @@ -8,7 +8,7 @@ import { GlSearchBoxByType, } from '@gitlab/ui'; import { kebabCase, snakeCase } from 'lodash'; -import { IssuableType, TYPE_ISSUE, WorkspaceType } from '~/issues/constants'; +import { IssuableType, TYPE_EPIC, TYPE_ISSUE, WorkspaceType } from '~/issues/constants'; import { __ } from '~/locale'; import { defaultEpicSort, @@ -155,7 +155,7 @@ export default { }, isEpic() { // MV to EE https://gitlab.com/gitlab-org/gitlab/-/issues/345311 - return this.issuableAttribute === IssuableType.Epic; + return this.issuableAttribute === TYPE_EPIC; }, issuableAttributeQuery() { return this.issuableAttributesQueries[this.issuableAttribute]; diff --git a/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue b/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue index 994b96b7f30..5df65c4aaaf 100644 --- a/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue +++ b/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue @@ -3,7 +3,7 @@ import { GlButton, GlIcon, GlLink, GlPopover, GlTooltipDirective } from '@gitlab import { kebabCase, snakeCase } from 'lodash'; import { createAlert } from '~/flash'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; -import { IssuableType, TYPE_ISSUE } from '~/issues/constants'; +import { IssuableType, TYPE_EPIC, TYPE_ISSUE } from '~/issues/constants'; import { timeFor } from '~/lib/utils/datetime_utility'; import { __ } from '~/locale'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; @@ -153,7 +153,7 @@ export default { }, isEpic() { // MV to EE https://gitlab.com/gitlab-org/gitlab/-/issues/345311 - return this.issuableAttribute === IssuableType.Epic; + return this.issuableAttribute === TYPE_EPIC; }, formatIssuableAttribute() { return { diff --git a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue index 0fba1cb5e4e..cbe839d1112 100644 --- a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue +++ b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue @@ -1,7 +1,7 @@ <script> import { GlDropdownForm, GlIcon, GlLoadingIcon, GlToggle, GlTooltipDirective } from '@gitlab/ui'; import { createAlert } from '~/flash'; -import { IssuableType } from '~/issues/constants'; +import { IssuableType, TYPE_EPIC } from '~/issues/constants'; import { isLoggedIn } from '~/lib/utils/common_utils'; import { __, sprintf } from '~/locale'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; @@ -105,7 +105,7 @@ export default { return ICON_ON; }, parentIsGroup() { - return this.issuableType === IssuableType.Epic; + return this.issuableType === TYPE_EPIC; }, subscribeDisabledDescription() { return sprintf(__('Disabled by %{parent} owner'), { diff --git a/app/assets/javascripts/sidebar/constants.js b/app/assets/javascripts/sidebar/constants.js index b2d6a111a0e..14491226b15 100644 --- a/app/assets/javascripts/sidebar/constants.js +++ b/app/assets/javascripts/sidebar/constants.js @@ -3,7 +3,7 @@ import { s__, __, sprintf } from '~/locale'; import updateIssueLabelsMutation from '~/boards/graphql/issue_set_labels.mutation.graphql'; import userSearchQuery from '~/graphql_shared/queries/users_search.query.graphql'; import userSearchWithMRPermissionsQuery from '~/graphql_shared/queries/users_search_with_mr_permissions.graphql'; -import { IssuableType, TYPE_ISSUE, WorkspaceType } from '~/issues/constants'; +import { IssuableType, TYPE_EPIC, TYPE_ISSUE, WorkspaceType } from '~/issues/constants'; import updateAlertAssigneesMutation from '~/vue_shared/alert_details/graphql/mutations/alert_set_assignees.mutation.graphql'; import updateTestCaseLabelsMutation from './components/labels/labels_select_widget/graphql/update_test_case_labels.mutation.graphql'; import epicLabelsQuery from './components/labels/labels_select_widget/graphql/epic_labels.query.graphql'; @@ -86,7 +86,7 @@ export const participantsQueries = { [IssuableType.MergeRequest]: { query: getMergeRequestParticipants, }, - [IssuableType.Epic]: { + [TYPE_EPIC]: { query: epicParticipantsQuery, }, [IssuableType.Alert]: { @@ -109,7 +109,7 @@ export const confidentialityQueries = { query: issueConfidentialQuery, mutation: updateIssueConfidentialMutation, }, - [IssuableType.Epic]: { + [TYPE_EPIC]: { query: epicConfidentialQuery, mutation: updateEpicConfidentialMutation, }, @@ -122,7 +122,7 @@ export const referenceQueries = { [IssuableType.MergeRequest]: { query: mergeRequestReferenceQuery, }, - [IssuableType.Epic]: { + [TYPE_EPIC]: { query: epicReferenceQuery, }, }; @@ -147,7 +147,7 @@ export const issuableLabelsQueries = { mutation: updateMergeRequestLabelsMutation, mutationName: 'mergeRequestSetLabels', }, - [IssuableType.Epic]: { + [TYPE_EPIC]: { issuableQuery: epicLabelsQuery, mutation: updateEpicLabelsMutation, mutationName: 'updateEpic', @@ -182,7 +182,7 @@ export const subscribedQueries = { query: issueSubscribedQuery, mutation: updateIssueSubscriptionMutation, }, - [IssuableType.Epic]: { + [TYPE_EPIC]: { query: epicSubscribedQuery, mutation: updateEpicSubscriptionMutation, }, @@ -211,14 +211,14 @@ export const dueDateQueries = { query: issueDueDateQuery, mutation: updateIssueDueDateMutation, }, - [IssuableType.Epic]: { + [TYPE_EPIC]: { query: epicDueDateQuery, mutation: updateEpicDueDateMutation, }, }; export const startDateQueries = { - [IssuableType.Epic]: { + [TYPE_EPIC]: { query: epicStartDateQuery, mutation: updateEpicStartDateMutation, }, @@ -283,7 +283,7 @@ export const issuableAttributesQueries = { }; export const todoQueries = { - [IssuableType.Epic]: { + [TYPE_EPIC]: { query: epicTodoQuery, }, [TYPE_ISSUE]: { diff --git a/app/assets/javascripts/super_sidebar/components/help_center.vue b/app/assets/javascripts/super_sidebar/components/help_center.vue index ed4b0d4772e..8e7c7efa631 100644 --- a/app/assets/javascripts/super_sidebar/components/help_center.vue +++ b/app/assets/javascripts/super_sidebar/components/help_center.vue @@ -1,5 +1,6 @@ <script> import { GlBadge, GlButton, GlDisclosureDropdown, GlDisclosureDropdownGroup } from '@gitlab/ui'; +import GitlabVersionCheckBadge from '~/gitlab_version_check/components/gitlab_version_check_badge.vue'; import { helpPagePath } from '~/helpers/help_page_helper'; import { PROMO_URL } from 'jh_else_ce/lib/utils/url_utility'; import { __ } from '~/locale'; @@ -11,6 +12,7 @@ export default { GlButton, GlDisclosureDropdown, GlDisclosureDropdownGroup, + GitlabVersionCheckBadge, }, i18n: { help: __('Help'), @@ -21,6 +23,7 @@ export default { contribute: __('Contribute to GitLab'), feedback: __('Provide feedback'), shortcuts: __('Keyboard shortcuts'), + version: __('Your GitLab version'), whatsnew: __("What's new"), }, props: { @@ -35,9 +38,18 @@ export default { }; }, computed: { - items() { - return [ - { + itemGroups() { + return { + versionCheck: { + items: [ + { + text: this.$options.i18n.version, + href: helpPagePath('update/index'), + version: `${this.sidebarData.gitlab_version.major}.${this.sidebarData.gitlab_version.minor}`, + }, + ], + }, + helpLinks: { items: [ { text: this.$options.i18n.help, href: helpPagePath() }, { text: this.$options.i18n.support, href: this.sidebarData.support_path }, @@ -51,7 +63,7 @@ export default { { text: this.$options.i18n.feedback, href: 'https://about.gitlab.com/submit-feedback' }, ], }, - { + helpActions: { items: [ { text: this.$options.i18n.shortcuts, @@ -67,7 +79,10 @@ export default { }, ].filter(Boolean), }, - ]; + }; + }, + updateSeverity() { + return this.sidebarData.gitlab_version_check?.severity; }, }, methods: { @@ -120,8 +135,34 @@ export default { </gl-button> </template> - <gl-disclosure-dropdown-group :group="items[0]" /> - <gl-disclosure-dropdown-group :group="items[1]" bordered @action="handleAction"> + <gl-disclosure-dropdown-group + v-if="sidebarData.show_version_check" + :group="itemGroups.versionCheck" + > + <template #list-item="{ item }"> + <a + :href="item.href" + tabindex="-1" + class="gl-display-flex gl-flex-direction-column gl-line-height-24 gl-text-gray-900 gl-hover-text-gray-900 gl-hover-text-decoration-none" + > + <span class="gl-font-sm gl-font-weight-bold"> + {{ item.text }} + <gl-emoji data-name="rocket" /> + </span> + <span> + <span class="gl-mr-2">{{ item.version }}</span> + <gitlab-version-check-badge v-if="updateSeverity" :status="updateSeverity" size="sm" /> + </span> + </a> + </template> + </gl-disclosure-dropdown-group> + + <gl-disclosure-dropdown-group + :group="itemGroups.helpLinks" + :bordered="sidebarData.show_version_check" + /> + + <gl-disclosure-dropdown-group :group="itemGroups.helpActions" bordered @action="handleAction"> <template #list-item="{ item }"> <button tabindex="-1" diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue index 1093e7cb2ee..2dec95c3fda 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue @@ -11,6 +11,7 @@ import { import SafeHtml from '~/vue_shared/directives/safe_html'; import { s__, n__ } from '~/locale'; import CiIcon from '~/vue_shared/components/ci_icon.vue'; +import { keepLatestDownstreamPipelines } from '~/pipelines/components/parsing_utils'; import PipelineArtifacts from '~/pipelines/components/pipelines_list/pipelines_artifacts.vue'; import PipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; @@ -86,6 +87,10 @@ export default { }, }, computed: { + downstreamPipelines() { + const downstream = this.pipeline.triggered; + return keepLatestDownstreamPipelines(downstream); + }, hasPipeline() { return this.pipeline && Object.keys(this.pipeline).length > 0; }, @@ -274,7 +279,7 @@ export default { <span class="gl-align-items-center gl-display-inline-flex"> <pipeline-mini-graph v-if="pipeline.details.stages" - :downstream-pipelines="pipeline.triggered" + :downstream-pipelines="downstreamPipelines" :is-merge-train="isMergeTrain" :pipeline-path="pipeline.path" :stages="pipeline.details.stages" diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb index 980275147ae..0ec23a3265f 100644 --- a/app/controllers/concerns/issuable_actions.rb +++ b/app/controllers/concerns/issuable_actions.rb @@ -90,7 +90,7 @@ module IssuableActions end def destroy - Issuable::DestroyService.new(project: issuable.project, current_user: current_user).execute(issuable) + Issuable::DestroyService.new(container: issuable.project, current_user: current_user).execute(issuable) name = issuable.human_class_name flash[:notice] = "The #{name} was successfully deleted." diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 3731257033d..97d0fa43529 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -191,7 +191,7 @@ class Projects::IssuesController < Projects::ApplicationController new_project = Project.find(params[:move_to_project_id]) return render_404 unless issue.can_move?(current_user, new_project) - @issue = ::Issues::MoveService.new(project: project, current_user: current_user).execute(issue, new_project) + @issue = ::Issues::MoveService.new(container: project, current_user: current_user).execute(issue, new_project) end respond_to do |format| diff --git a/app/graphql/mutations/issues/move.rb b/app/graphql/mutations/issues/move.rb index 63bc9dabbf9..ef3f70c78b9 100644 --- a/app/graphql/mutations/issues/move.rb +++ b/app/graphql/mutations/issues/move.rb @@ -18,7 +18,7 @@ module Mutations target_project = resolve_project(full_path: target_project_path).sync begin - moved_issue = ::Issues::MoveService.new(project: source_project, current_user: current_user).execute(issue, target_project) + moved_issue = ::Issues::MoveService.new(container: source_project, current_user: current_user).execute(issue, target_project) rescue ::Issues::MoveService::MoveError => e errors = e.message end diff --git a/app/graphql/mutations/work_items/delete.rb b/app/graphql/mutations/work_items/delete.rb index 4b0067d40d4..ec0244fa65e 100644 --- a/app/graphql/mutations/work_items/delete.rb +++ b/app/graphql/mutations/work_items/delete.rb @@ -20,7 +20,7 @@ module Mutations work_item = authorized_find!(id: id) result = ::WorkItems::DeleteService.new( - project: work_item.project, + container: work_item.project, current_user: current_user ).execute(work_item) diff --git a/app/graphql/types/issue_type.rb b/app/graphql/types/issue_type.rb index fa102c7b496..de0d5cc417d 100644 --- a/app/graphql/types/issue_type.rb +++ b/app/graphql/types/issue_type.rb @@ -65,6 +65,13 @@ module Types field :merge_requests_count, GraphQL::Types::Int, null: false, description: 'Number of merge requests that close the issue on merge.', resolver: Resolvers::MergeRequestsCountResolver + + field :related_merge_requests, Types::MergeRequestType.connection_type, + null: true, + description: 'Merge requests related to the issue. This field can only be resolved for one issue in any single request.' do + extension ::Gitlab::Graphql::Limit::FieldCallCount, limit: 1 + end + field :relative_position, GraphQL::Types::Int, null: true, description: 'Relative position of the issue (used for positioning in epic tree and issue boards).' field :upvotes, GraphQL::Types::Int, @@ -180,6 +187,17 @@ module Types Gitlab::Graphql::Loaders::BatchModelLoader.new(Issue, object.duplicated_to_id).find end + def related_merge_requests + # rubocop: disable CodeReuse/ActiveRecord + MergeRequest.where( + id: ::Issues::ReferencedMergeRequestsService.new(project: object.project, current_user: current_user) + .execute(object) + .first + .map(&:id) + ) + # rubocop: enable CodeReuse/ActiveRecord + end + def discussion_locked !!object.discussion_locked end diff --git a/app/helpers/sidebars_helper.rb b/app/helpers/sidebars_helper.rb index 733792cdf0f..27020738515 100644 --- a/app/helpers/sidebars_helper.rb +++ b/app/helpers/sidebars_helper.rb @@ -48,7 +48,10 @@ module SidebarsHelper support_path: support_url, display_whats_new: display_whats_new?, whats_new_most_recent_release_items_count: whats_new_most_recent_release_items_count, - whats_new_version_digest: whats_new_version_digest + whats_new_version_digest: whats_new_version_digest, + show_version_check: show_version_check?, + gitlab_version: Gitlab.version_info, + gitlab_version_check: gitlab_version_check } end diff --git a/app/helpers/web_hooks/web_hooks_helper.rb b/app/helpers/web_hooks/web_hooks_helper.rb index bda9bf58fb7..514db6ba8a2 100644 --- a/app/helpers/web_hooks/web_hooks_helper.rb +++ b/app/helpers/web_hooks/web_hooks_helper.rb @@ -2,8 +2,6 @@ module WebHooks module WebHooksHelper - EXPIRY_TTL = 1.hour - def show_project_hook_failed_callout?(project:) return false if project_hook_page? return false unless current_user @@ -12,17 +10,11 @@ module WebHooks # Assumes include of Users::CalloutsHelper return false if web_hook_disabled_dismissed?(project) - any_project_hook_failed?(project) # Most expensive query last + project.fetch_web_hook_failure end private - def any_project_hook_failed?(project) - Rails.cache.fetch("any_web_hook_failed:#{project.id}", expires_in: EXPIRY_TTL) do - ProjectHook.for_projects(project).disabled.exists? - end - end - def project_hook_page? current_controller?('projects/hooks') || current_controller?('projects/hook_logs') end diff --git a/app/models/concerns/web_hooks/has_web_hooks.rb b/app/models/concerns/web_hooks/has_web_hooks.rb new file mode 100644 index 00000000000..161ce106b9b --- /dev/null +++ b/app/models/concerns/web_hooks/has_web_hooks.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module WebHooks + module HasWebHooks + extend ActiveSupport::Concern + + WEB_HOOK_CACHE_EXPIRY = 1.hour + + def any_hook_failed? + hooks.disabled.exists? + end + + def web_hook_failure_redis_key + "any_web_hook_failed:#{id}" + end + + def last_failure_redis_key + "web_hooks:last_failure:project-#{id}" + end + + def get_web_hook_failure + Gitlab::Redis::SharedState.with do |redis| + current = redis.get(web_hook_failure_redis_key) + + Gitlab::Utils.to_boolean(current) if current + end + end + + def fetch_web_hook_failure + Gitlab::Redis::SharedState.with do |_redis| + current = get_web_hook_failure + next current unless current.nil? + + cache_web_hook_failure + end + end + + def cache_web_hook_failure(state = any_hook_failed?) + Gitlab::Redis::SharedState.with do |redis| + redis.set(web_hook_failure_redis_key, state.to_s, ex: WEB_HOOK_CACHE_EXPIRY) + + state + end + end + end +end diff --git a/app/models/hooks/project_hook.rb b/app/models/hooks/project_hook.rb index 81122c3ea10..8e9a74a68d0 100644 --- a/app/models/hooks/project_hook.rb +++ b/app/models/hooks/project_hook.rb @@ -46,14 +46,18 @@ class ProjectHook < WebHook override :update_last_failure def update_last_failure - return if executable? + if executable? + project.cache_web_hook_failure if project.get_web_hook_failure # may need update + else + project.cache_web_hook_failure(true) # definitely failing, no need to check - key = "web_hooks:last_failure:project-#{project_id}" - time = Time.current.utc.iso8601 + Gitlab::Redis::SharedState.with do |redis| + last_failure_key = project.last_failure_redis_key + time = Time.current.utc.iso8601 + prev = redis.get(last_failure_key) - Gitlab::Redis::SharedState.with do |redis| - prev = redis.get(key) - redis.set(key, time) if !prev || prev < time + redis.set(last_failure_key, time) if !prev || prev < time + end end end end diff --git a/app/models/project.rb b/app/models/project.rb index 27be84d787e..63ea19202ce 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -41,6 +41,7 @@ class Project < ApplicationRecord include BlocksUnsafeSerialization include Subquery include IssueParent + include WebHooks::HasWebHooks extend Gitlab::Cache::RequestCache extend Gitlab::Utils::Override diff --git a/app/models/repository.rb b/app/models/repository.rb index 31c65555cb2..d15f2a430fa 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -872,10 +872,24 @@ class Repository end def merge(user, source_sha, merge_request, message) + merge_to_branch(user, + source_sha: source_sha, + target_branch: merge_request.target_branch, + message: message) do |commit_id| + merge_request.update_and_mark_in_progress_merge_commit_sha(commit_id) + nil # Return value does not matter. + end + end + + def merge_to_branch(user, source_sha:, target_branch:, message:, target_sha: nil) with_cache_hooks do - raw_repository.merge(user, source_sha, merge_request.target_branch, message) do |commit_id| - merge_request.update_and_mark_in_progress_merge_commit_sha(commit_id) - nil # Return value does not matter. + raw_repository.merge(user, + source_sha: source_sha, + target_branch: target_branch, + message: message, + target_sha: target_sha + ) do |commit_id| + yield commit_id if block_given? end end end @@ -884,13 +898,19 @@ class Repository raw.delete_refs(...) end - def ff_merge(user, source, target_branch, merge_request: nil) + def ff_merge(user, source, target_branch, target_sha: nil, merge_request: nil) their_commit_id = commit(source)&.id raise 'Invalid merge source' if their_commit_id.nil? merge_request&.update_and_mark_in_progress_merge_commit_sha(their_commit_id) - with_cache_hooks { raw.ff_merge(user, their_commit_id, target_branch) } + with_cache_hooks do + raw.ff_merge(user, + source_sha: their_commit_id, + target_branch: target_branch, + target_sha: target_sha + ) + end end def revert( diff --git a/app/models/user_synced_attributes_metadata.rb b/app/models/user_synced_attributes_metadata.rb index 5aacf11b1cb..4cceffda19e 100644 --- a/app/models/user_synced_attributes_metadata.rb +++ b/app/models/user_synced_attributes_metadata.rb @@ -14,7 +14,7 @@ class UserSyncedAttributesMetadata < ApplicationRecord def read_only_attributes return [] unless sync_profile_from_provider? - SYNCABLE_ATTRIBUTES.select { |key| synced?(key) } + self.class.syncable_attributes.select { |key| synced?(key) } end def synced?(attribute) @@ -25,6 +25,20 @@ class UserSyncedAttributesMetadata < ApplicationRecord write_attribute("#{attribute}_synced", value) end + class << self + def syncable_attributes + return SYNCABLE_ATTRIBUTES if sync_name? + + SYNCABLE_ATTRIBUTES - %i[name] + end + + private + + def sync_name? + Gitlab.config.ldap.sync_name + end + end + private def sync_profile_from_provider? diff --git a/app/serializers/triggered_pipeline_entity.rb b/app/serializers/triggered_pipeline_entity.rb index 9fdadb322bf..d8d0e576565 100644 --- a/app/serializers/triggered_pipeline_entity.rb +++ b/app/serializers/triggered_pipeline_entity.rb @@ -15,6 +15,9 @@ class TriggeredPipelineEntity < Grape::Entity expose :name do |pipeline| pipeline.source_job&.name end + expose :retried do |pipeline| + pipeline.source_job&.retried + end end expose :path do |pipeline| diff --git a/app/services/design_management/copy_design_collection/copy_service.rb b/app/services/design_management/copy_design_collection/copy_service.rb index 3bc30f62a81..8074a193bbf 100644 --- a/app/services/design_management/copy_design_collection/copy_service.rb +++ b/app/services/design_management/copy_design_collection/copy_service.rb @@ -128,9 +128,9 @@ module DesignManagement target_repository.raw.merge( git_user, - source_sha, - merge_branch, - 'CopyDesignCollectionService finalize merge' + source_sha: source_sha, + target_branch: merge_branch, + message: 'CopyDesignCollectionService finalize merge' ) { nil } target_design_collection.end_copy! diff --git a/app/services/issuable/clone/base_service.rb b/app/services/issuable/clone/base_service.rb index 3c13944cfbc..3d0abece1a2 100644 --- a/app/services/issuable/clone/base_service.rb +++ b/app/services/issuable/clone/base_service.rb @@ -7,6 +7,11 @@ module Issuable alias_method :old_project, :project + # TODO: this is to be removed once we get to rename the IssuableBaseService project param to container + def initialize(container:, current_user: nil, params: {}) + super(project: container, current_user: current_user, params: params) + end + def execute(original_entity, target_parent) @original_entity = original_entity @target_parent = target_parent diff --git a/app/services/issuable/destroy_service.rb b/app/services/issuable/destroy_service.rb index 6aab56f0f68..4c3e518d62b 100644 --- a/app/services/issuable/destroy_service.rb +++ b/app/services/issuable/destroy_service.rb @@ -2,6 +2,11 @@ module Issuable class DestroyService < IssuableBaseService + # TODO: this is to be removed once we get to rename the IssuableBaseService project param to container + def initialize(container:, current_user: nil, params: {}) + super(project: container, current_user: current_user, params: params) + end + def execute(issuable) after_destroy(issuable) if issuable.destroy end diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb index d43df0da3fd..a185ff374ed 100644 --- a/app/services/issues/update_service.rb +++ b/app/services/issues/update_service.rb @@ -109,7 +109,7 @@ module Issues target_project != issue.project update(issue) - Issues::MoveService.new(project: project, current_user: current_user).execute(issue, target_project) + Issues::MoveService.new(container: project, current_user: current_user).execute(issue, target_project) end private @@ -139,7 +139,7 @@ module Issues # we've pre-empted this from running in #execute, so let's go ahead and update the Issue now. update(issue) - Issues::CloneService.new(project: project, current_user: current_user).execute(issue, target_project, with_notes: with_notes) + Issues::CloneService.new(container: project, current_user: current_user).execute(issue, target_project, with_notes: with_notes) end def create_merge_request_from_quick_action diff --git a/app/services/tasks_to_be_done/base_service.rb b/app/services/tasks_to_be_done/base_service.rb index a5648ad10c4..c868ebcc463 100644 --- a/app/services/tasks_to_be_done/base_service.rb +++ b/app/services/tasks_to_be_done/base_service.rb @@ -4,14 +4,14 @@ module TasksToBeDone class BaseService < ::IssuableBaseService LABEL_PREFIX = 'tasks to be done' - def initialize(project:, current_user:, assignee_ids: []) + def initialize(container:, current_user:, assignee_ids: []) params = { assignee_ids: assignee_ids, title: title, description: description, add_labels: label_name } - super(project: project, current_user: current_user, params: params) + super(project: container, current_user: current_user, params: params) end def execute diff --git a/app/services/work_items/delete_task_service.rb b/app/services/work_items/delete_task_service.rb index 2a82a993b71..3d66716543a 100644 --- a/app/services/work_items/delete_task_service.rb +++ b/app/services/work_items/delete_task_service.rb @@ -25,7 +25,7 @@ module WorkItems break ::ServiceResponse.error(message: replacement_result.errors, http_status: 422) if replacement_result.error? delete_result = ::WorkItems::DeleteService.new( - project: @task.project, + container: @task.project, current_user: @current_user ).execute(@task) diff --git a/app/views/projects/mirrors/_branch_filter.html.haml b/app/views/projects/mirrors/_branch_filter.html.haml new file mode 100644 index 00000000000..b9db9898d49 --- /dev/null +++ b/app/views/projects/mirrors/_branch_filter.html.haml @@ -0,0 +1,6 @@ +.form-check.gl-mb-3 + = check_box_tag :only_protected_branches, '1', false, class: 'js-mirror-protected form-check-input' + = label_tag :only_protected_branches, _('Mirror only protected branches'), class: 'form-check-label' + .form-text.text-muted + = _('If enabled, only protected branches will be mirrored.') + = link_to _('Learn more.'), help_page_path('user/project/repository/mirror/index.md', anchor: 'mirror-only-protected-branches'), target: '_blank', rel: 'noopener noreferrer' diff --git a/app/views/projects/mirrors/_mirror_repos.html.haml b/app/views/projects/mirrors/_mirror_repos.html.haml index f4e57450aa1..4cfe463fa38 100644 --- a/app/views/projects/mirrors/_mirror_repos.html.haml +++ b/app/views/projects/mirrors/_mirror_repos.html.haml @@ -27,12 +27,7 @@ = render 'projects/mirrors/mirror_repos_form', f: f - .form-check.gl-mb-3 - = check_box_tag :only_protected_branches, '1', false, class: 'js-mirror-protected form-check-input' - = label_tag :only_protected_branches, _('Mirror only protected branches'), class: 'form-check-label' - .form-text.text-muted - = _('If enabled, only protected branches will be mirrored.') - = link_to _('Learn more.'), help_page_path('user/project/repository/mirror/index.md', anchor: 'mirror-only-protected-branches'), target: '_blank', rel: 'noopener noreferrer' + = render 'projects/mirrors/branch_filter' .panel-footer = f.submit _('Mirror repository'), class: 'js-mirror-submit', name: :update_remote_mirror, pajamas_button: true, data: { qa_selector: 'mirror_repository_button' } diff --git a/app/views/projects/mirrors/_mirror_repos_list.html.haml b/app/views/projects/mirrors/_mirror_repos_list.html.haml index fb8133e6de8..46833b5986b 100644 --- a/app/views/projects/mirrors/_mirror_repos_list.html.haml +++ b/app/views/projects/mirrors/_mirror_repos_list.html.haml @@ -26,7 +26,9 @@ - @project.remote_mirrors.each_with_index do |mirror, index| - next if mirror.new_record? %tr.rspec-mirrored-repository-row{ class: ('bg-secondary' if mirror.disabled?), data: { qa_selector: 'mirrored_repository_row_container' } } - %td{ data: { qa_selector: 'mirror_repository_url_content' } }= mirror.safe_url || _('Invalid URL') + %td{ data: { qa_selector: 'mirror_repository_url_content' } } + = mirror.safe_url || _('Invalid URL') + = render_if_exists 'projects/mirrors/mirror_branches_setting_badge', record: mirror %td= _('Push') %td = mirror.last_update_started_at.present? ? time_ago_with_tooltip(mirror.last_update_started_at) : _('Never') diff --git a/app/views/projects/mirrors/_mirror_repos_push.html.haml b/app/views/projects/mirrors/_mirror_repos_push.html.haml index 339c5d82919..136f504084e 100644 --- a/app/views/projects/mirrors/_mirror_repos_push.html.haml +++ b/app/views/projects/mirrors/_mirror_repos_push.html.haml @@ -4,6 +4,7 @@ = rm_f.hidden_field :enabled, value: '1' = rm_f.hidden_field :url, class: 'js-mirror-url-hidden', required: true, pattern: "(#{protocols}):\/\/.+" = rm_f.hidden_field :only_protected_branches, class: 'js-mirror-protected-hidden' + = render_if_exists partial: 'projects/mirrors/branch_name_regex', locals: { f: rm_f } = rm_f.hidden_field :keep_divergent_refs, class: 'js-mirror-keep-divergent-refs-hidden' = render partial: 'projects/mirrors/ssh_host_keys', locals: { f: rm_f } = render partial: 'projects/mirrors/authentication_method', locals: { f: rm_f } diff --git a/app/workers/tasks_to_be_done/create_worker.rb b/app/workers/tasks_to_be_done/create_worker.rb index 0953f190fd0..d3824ceb4ae 100644 --- a/app/workers/tasks_to_be_done/create_worker.rb +++ b/app/workers/tasks_to_be_done/create_worker.rb @@ -17,7 +17,7 @@ module TasksToBeDone member_task.tasks_to_be_done.each do |task| service_class(task) - .new(project: project, current_user: current_user, assignee_ids: assignee_ids) + .new(container: project, current_user: current_user, assignee_ids: assignee_ids) .execute end end diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index a2798bfda4d..1927d15d72b 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -27,6 +27,7 @@ end # backwards compatibility, we only have one host if Settings.ldap['enabled'] || Rails.env.test? + Settings.ldap['sync_name'] = true if Settings.ldap['sync_name'].nil? if Settings.ldap['host'].present? # We detected old LDAP configuration syntax. Update the config to make it # look like it was entered with the new syntax. diff --git a/config/initializers/countries.rb b/config/initializers/countries.rb index e22637f459c..98d06c9a893 100644 --- a/config/initializers/countries.rb +++ b/config/initializers/countries.rb @@ -8,57 +8,11 @@ end # This overrides the display name for Ukraine to 'Ukraine (except the Crimea, Donetsk, and Luhansk regions)' # See: https://gitlab.com/gitlab-org/gitlab/-/issues/374946 # To be removed after https://gitlab.com/gitlab-org/gitlab/issues/14784 is implemented -# Data fetched is based on https://github.com/countries/countries/blob/master/lib/countries/data/countries/UA.yaml ISO3166::Data.register( - continent: "Europe", - address_format: "|- - {{recipient}} - {{street}} - {{city}} {{region_short}} - {{postalcode}} - {{country}}", - alpha2: "UA", - alpha3: "UKR", - country_code: '380', - international_prefix: '810', - ioc: "UKR", - gec: "UP", - name: "Ukraine (except the Crimea, Donetsk, and Luhansk regions)", - national_destination_code_lengths: [2], - national_number_lengths: [8, 9], - national_prefix: '8', - number: '804', - region: "Europe", - subregion: "Eastern Europe", - world_region: "EMEA", - un_locode: "UA", - nationality: "Ukrainian", - vat_rates: { - standard: 20 - }, - reduced: [7], - super_reduced: { - parking: { postal_code: true } - }, - unofficial_names: %w(Ukraine Ucrania ウクライナ Oekraïne Украина Україна Украіна), - languages_official: ["uk"], - languages_spoken: ["uk"], - geo: { - latitude: 48.379433, - latitude_dec: '48.92656326293945', - longitude: 31.16558, - longitude_dec: '31.47578239440918', - max_latitude: 52.37958099999999, - max_longitude: 40.2285809, - min_latitude: 44.2924, - min_longitude: 22.137159, - bounds: { - northeast: { lat: 52.37958099999999, lng: 40.2285809 }, - southwest: { lat: 44.2924, lng: 22.137159 } - } - }, - currency_code: "UAH", - start_of_week: "monday" + ISO3166::Data.new('UA') + .call + .deep_symbolize_keys + .merge({ name: 'Ukraine (except the Crimea, Donetsk, and Luhansk regions)' }) ) # Updating the display name of Taiwan, from `Taiwan, Province of China` to `Taiwan` diff --git a/data/deprecations/15-9-deprecate-external-field-in-graphql-release-asset-link.yml b/data/deprecations/15-9-deprecate-external-field-in-graphql-release-asset-link.yml new file mode 100644 index 00000000000..3cc9505a0b1 --- /dev/null +++ b/data/deprecations/15-9-deprecate-external-field-in-graphql-release-asset-link.yml @@ -0,0 +1,13 @@ +- title: "External field in GraphQL ReleaseAssetLink type" + announcement_milestone: "15.9" + announcement_date: "2023-02-22" + removal_milestone: "16.0" + removal_date: "2023-05-22" + breaking_change: true + reporter: ahmed.hemdan + body: | + In the [GraphQL API](https://docs.gitlab.com/ee/api/graphql), the `external` field of [`ReleaseAssetLink` type](https://docs.gitlab.com/ee/api/graphql/reference/index.html#releaseassetlink) was used to indicate whether a [release link](https://docs.gitlab.com/ee/user/project/releases/release_fields.html#links) is internal or external to your GitLab instance. + As of GitLab 15.9, we treat all release links as external, and therefore, this field is deprecated in GitLab 15.9, and will be removed in GitLab 16.0. + To avoid any disruptions to your workflow, please stop using the `external` field because it will be removed and will not be replaced. + stage: Release + issue_url: diff --git a/db/post_migrate/20230209131808_recount_epic_cache_counts_v3.rb b/db/post_migrate/20230209131808_recount_epic_cache_counts_v3.rb new file mode 100644 index 00000000000..6252a300453 --- /dev/null +++ b/db/post_migrate/20230209131808_recount_epic_cache_counts_v3.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +class RecountEpicCacheCountsV3 < Gitlab::Database::Migration[2.1] + MIGRATION = 'ThirdRecountEpicCacheCounts' + DELAY_INTERVAL = 2.minutes.to_i + BATCH_SIZE = 200 + MAX_BATCH_SIZE = 1000 + SUB_BATCH_SIZE = 20 + + disable_ddl_transaction! + restrict_gitlab_migration gitlab_schema: :gitlab_main + + def up + queue_batched_background_migration( + MIGRATION, + :epics, + :id, + job_interval: DELAY_INTERVAL, + batch_size: BATCH_SIZE, + max_batch_size: MAX_BATCH_SIZE, + sub_batch_size: SUB_BATCH_SIZE, + gitlab_schema: :gitlab_main + ) + end + + def down + delete_batched_background_migration(MIGRATION, :epics, :id, []) + end +end diff --git a/db/schema_migrations/20230209131808 b/db/schema_migrations/20230209131808 new file mode 100644 index 00000000000..ba90b9dcd50 --- /dev/null +++ b/db/schema_migrations/20230209131808 @@ -0,0 +1 @@ +48c3039b24ab063a550419d3883b3c6308709e0ef9eacc0f4f1cdc3c99fb4148
\ No newline at end of file diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 7a8c383a341..6765adcca87 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -13263,6 +13263,7 @@ Relationship between an epic and an issue. | <a id="epicissuenotes"></a>`notes` | [`NoteConnection!`](#noteconnection) | All notes on this noteable. (see [Connections](#connections)) | | <a id="epicissueparticipants"></a>`participants` | [`UserCoreConnection`](#usercoreconnection) | List of participants in the issue. (see [Connections](#connections)) | | <a id="epicissueprojectid"></a>`projectId` | [`Int!`](#int) | ID of the issue project. | +| <a id="epicissuerelatedmergerequests"></a>`relatedMergeRequests` | [`MergeRequestConnection`](#mergerequestconnection) | Merge requests related to the issue. This field can only be resolved for one issue in any single request. (see [Connections](#connections)) | | <a id="epicissuerelatedvulnerabilities"></a>`relatedVulnerabilities` | [`VulnerabilityConnection`](#vulnerabilityconnection) | Related vulnerabilities of the issue. (see [Connections](#connections)) | | <a id="epicissuerelationpath"></a>`relationPath` | [`String`](#string) | URI path of the epic-issue relation. | | <a id="epicissuerelativeposition"></a>`relativePosition` | [`Int`](#int) | Relative position of the issue (used for positioning in epic tree and issue boards). | @@ -15009,6 +15010,7 @@ Describes an issuable resource link for incident issues. | <a id="issuenotes"></a>`notes` | [`NoteConnection!`](#noteconnection) | All notes on this noteable. (see [Connections](#connections)) | | <a id="issueparticipants"></a>`participants` | [`UserCoreConnection`](#usercoreconnection) | List of participants in the issue. (see [Connections](#connections)) | | <a id="issueprojectid"></a>`projectId` | [`Int!`](#int) | ID of the issue project. | +| <a id="issuerelatedmergerequests"></a>`relatedMergeRequests` | [`MergeRequestConnection`](#mergerequestconnection) | Merge requests related to the issue. This field can only be resolved for one issue in any single request. (see [Connections](#connections)) | | <a id="issuerelatedvulnerabilities"></a>`relatedVulnerabilities` | [`VulnerabilityConnection`](#vulnerabilityconnection) | Related vulnerabilities of the issue. (see [Connections](#connections)) | | <a id="issuerelativeposition"></a>`relativePosition` | [`Int`](#int) | Relative position of the issue (used for positioning in epic tree and issue boards). | | <a id="issueseverity"></a>`severity` | [`IssuableSeverity`](#issuableseverity) | Severity level of the incident. | diff --git a/doc/update/deprecations.md b/doc/update/deprecations.md index 5d124bfe045..59c2d1d923b 100644 --- a/doc/update/deprecations.md +++ b/doc/update/deprecations.md @@ -161,6 +161,22 @@ During the transition to the GitLab Observability UI, we will migrate the [GitLa <div class="deprecation removal-160 breaking-change"> +### External field in GraphQL ReleaseAssetLink type + +Planned removal: GitLab <span class="removal-milestone">16.0</span> <span class="removal-date"></span> + +WARNING: +This is a [breaking change](https://docs.gitlab.com/ee/development/deprecation_guidelines/). +Review the details carefully before upgrading. + +In the [GraphQL API](https://docs.gitlab.com/ee/api/graphql), the `external` field of [`ReleaseAssetLink` type](https://docs.gitlab.com/ee/api/graphql/reference/index.html#releaseassetlink) was used to indicate whether a [release link](https://docs.gitlab.com/ee/user/project/releases/release_fields.html#links) is internal or external to your GitLab instance. +As of GitLab 15.9, we treat all release links as external, and therefore, this field is deprecated in GitLab 15.9, and will be removed in GitLab 16.0. +To avoid any disruptions to your workflow, please stop using the `external` field because it will be removed and will not be replaced. + +</div> + +<div class="deprecation removal-160 breaking-change"> + ### External field in Releases and Release Links APIs Planned removal: GitLab <span class="removal-milestone">16.0</span> <span class="removal-date"></span> diff --git a/doc/user/admin_area/settings/email.md b/doc/user/admin_area/settings/email.md index 3cdb045d19d..484f51d8739 100644 --- a/doc/user/admin_area/settings/email.md +++ b/doc/user/admin_area/settings/email.md @@ -86,9 +86,7 @@ To disable these notifications: ### Custom additional text in deactivation emails **(FREE SELF)** -> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/355964) in GitLab 15.9 -[with a flag](../../../administration/feature_flags.md) named `deactivation_email_additional_text`. -Disabled by default. +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/355964) in GitLab 15.9 [with a flag](../../../administration/feature_flags.md) named `deactivation_email_additional_text`. Disabled by default. FLAG: On self-managed GitLab, by default this feature is not available. To make it available, ask an diff --git a/doc/user/project/issues/managing_issues.md b/doc/user/project/issues/managing_issues.md index fb7995b1915..158249c66a6 100644 --- a/doc/user/project/issues/managing_issues.md +++ b/doc/user/project/issues/managing_issues.md @@ -172,7 +172,7 @@ To do it: issues.each do |issue| if issue.state != "closed" && issue.moved_to.nil? - Issues::MoveService.new(project: project, current_user: admin_user).execute(issue, target_project) + Issues::MoveService.new(container: project, current_user: admin_user).execute(issue, target_project) else puts "issue with id: #{issue.id} and title: #{issue.title} was not moved" end diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md index 0276f3d98b6..a633366cc62 100644 --- a/doc/user/project/merge_requests/index.md +++ b/doc/user/project/merge_requests/index.md @@ -378,5 +378,5 @@ with a backup of the instance ready to be restored, just in case. u = User.find_by_username('<username>') p = Project.find_by_full_path('<namespace/project>') m = p.merge_requests.find_by(iid: <iid>) -Issuable::DestroyService.new(project: m.project, current_user: u).execute(m) +Issuable::DestroyService.new(container: m.project, current_user: u).execute(m) ``` diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 7ba055467fe..e8eb4bb1ddf 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -376,7 +376,7 @@ module API not_found!('Project') unless new_project begin - issue = ::Issues::MoveService.new(project: user_project, current_user: current_user).execute(issue, new_project) + issue = ::Issues::MoveService.new(container: user_project, current_user: current_user).execute(issue, new_project) present issue, with: Entities::Issue, current_user: current_user, project: user_project rescue ::Issues::MoveService::MoveError => error render_api_error!(error.message, 400) @@ -403,7 +403,7 @@ module API not_found!('Project') unless target_project begin - issue = ::Issues::CloneService.new(project: user_project, current_user: current_user) + issue = ::Issues::CloneService.new(container: user_project, current_user: current_user) .execute(issue, target_project, with_notes: params[:with_notes]) present issue, with: Entities::Issue, current_user: current_user, project: target_project rescue ::Issues::CloneService::CloneError => error @@ -424,7 +424,7 @@ module API authorize!(:destroy_issue, issue) destroy_conditionally!(issue) do |issue| - Issuable::DestroyService.new(project: user_project, current_user: current_user).execute(issue) + Issuable::DestroyService.new(container: user_project, current_user: current_user).execute(issue) end end # rubocop: enable CodeReuse/ActiveRecord diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index d4a445edfe2..cd46b442b68 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -312,7 +312,7 @@ module API authorize!(:destroy_merge_request, merge_request) destroy_conditionally!(merge_request) do |merge_request| - Issuable::DestroyService.new(project: user_project, current_user: current_user).execute(merge_request) + Issuable::DestroyService.new(container: user_project, current_user: current_user).execute(merge_request) end end diff --git a/lib/gitlab/auth/o_auth/user.rb b/lib/gitlab/auth/o_auth/user.rb index 242390c3e89..01e126ec2f5 100644 --- a/lib/gitlab/auth/o_auth/user.rb +++ b/lib/gitlab/auth/o_auth/user.rb @@ -258,7 +258,7 @@ module Gitlab metadata = gl_user.build_user_synced_attributes_metadata if sync_profile_from_provider? - UserSyncedAttributesMetadata::SYNCABLE_ATTRIBUTES.each do |key| + UserSyncedAttributesMetadata.syncable_attributes.each do |key| if auth_hash.has_attribute?(key) && gl_user.sync_attribute?(key) gl_user.public_send("#{key}=".to_sym, auth_hash.public_send(key)) # rubocop:disable GitlabSecurity/PublicSend metadata.set_attribute_synced(key, true) diff --git a/lib/gitlab/background_migration/third_recount_epic_cache_counts.rb b/lib/gitlab/background_migration/third_recount_epic_cache_counts.rb new file mode 100644 index 00000000000..24080a06c46 --- /dev/null +++ b/lib/gitlab/background_migration/third_recount_epic_cache_counts.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Gitlab + module BackgroundMigration + # rubocop: disable Style/Documentation + class ThirdRecountEpicCacheCounts < Gitlab::BackgroundMigration::BatchedMigrationJob + feature_category :database + + def perform; end + end + # rubocop: enable Style/Documentation + end +end + +# rubocop: disable Layout/LineLength +# we just want to re-enqueue the previous BackfillEpicCacheCounts migration, +# because it's a EE-only migation and it's a module, we just prepend new +# RecountEpicCacheCounts with existing batched migration module (which is same in both cases) +Gitlab::BackgroundMigration::ThirdRecountEpicCacheCounts.prepend_mod_with('Gitlab::BackgroundMigration::BackfillEpicCacheCounts') +# rubocop: enable Layout/LineLength diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 678e298c378..e054b6df98f 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -652,15 +652,23 @@ module Gitlab end end - def merge(user, source_sha, target_branch, message, &block) + def merge(user, source_sha:, target_branch:, message:, target_sha: nil, &block) wrapped_gitaly_errors do - gitaly_operation_client.user_merge_branch(user, source_sha, target_branch, message, &block) + gitaly_operation_client.user_merge_branch(user, + source_sha: source_sha, + target_branch: target_branch, + message: message, + target_sha: target_sha, + &block) end end - def ff_merge(user, source_sha, target_branch) + def ff_merge(user, source_sha:, target_branch:, target_sha: nil) wrapped_gitaly_errors do - gitaly_operation_client.user_ff_branch(user, source_sha, target_branch) + gitaly_operation_client.user_ff_branch(user, + source_sha: source_sha, + target_branch: target_branch, + target_sha: target_sha) end end diff --git a/lib/gitlab/gitaly_client/operation_service.rb b/lib/gitlab/gitaly_client/operation_service.rb index 6865e76d4bb..313334737c0 100644 --- a/lib/gitlab/gitaly_client/operation_service.rb +++ b/lib/gitlab/gitaly_client/operation_service.rb @@ -152,7 +152,7 @@ module Gitlab response.commit_id end - def user_merge_branch(user, source_sha, target_branch, message) + def user_merge_branch(user, source_sha:, target_branch:, message:, target_sha: nil) request_enum = QueueEnumerator.new response_enum = gitaly_client_call( @repository.storage, @@ -168,6 +168,7 @@ module Gitlab user: Gitlab::Git::User.from_gitlab(user).to_gitaly, commit_id: source_sha, branch: encode_binary(target_branch), + expected_old_oid: target_sha, message: encode_binary(message), timestamp: Google::Protobuf::Timestamp.new(seconds: Time.now.utc.to_i) ) @@ -184,7 +185,6 @@ module Gitlab raise Gitlab::Git::CommitError, 'failed to apply merge to branch' unless branch_update.commit_id.present? Gitlab::Git::OperationService::BranchUpdate.from_gitaly(branch_update) - rescue GRPC::BadStatus => e detailed_error = GitalyClient.decode_detailed_error(e) @@ -207,12 +207,13 @@ module Gitlab request_enum.close end - def user_ff_branch(user, source_sha, target_branch) + def user_ff_branch(user, source_sha:, target_branch:, target_sha: nil) request = Gitaly::UserFFBranchRequest.new( repository: @gitaly_repo, user: Gitlab::Git::User.from_gitlab(user).to_gitaly, commit_id: source_sha, - branch: encode_binary(target_branch) + branch: encode_binary(target_branch), + expected_old_oid: target_sha ) response = gitaly_client_call( diff --git a/lib/gitlab/redis/cache.rb b/lib/gitlab/redis/cache.rb index ba3af3e7a6f..647573e59fe 100644 --- a/lib/gitlab/redis/cache.rb +++ b/lib/gitlab/redis/cache.rb @@ -2,6 +2,16 @@ module Gitlab module Redis + # Match signature in + # https://github.com/rails/rails/blob/v6.1.7.2/activesupport/lib/active_support/cache/redis_cache_store.rb#L59 + ERROR_HANDLER = ->(method:, returning:, exception:) do + Gitlab::ErrorTracking.log_exception( + exception, + method: method, + returning: returning.inspect + ) + end + class Cache < ::Gitlab::Redis::Wrapper CACHE_NAMESPACE = 'cache:gitlab' @@ -12,7 +22,8 @@ module Gitlab redis: pool, compress: Gitlab::Utils.to_boolean(ENV.fetch('ENABLE_REDIS_CACHE_COMPRESSION', '1')), namespace: CACHE_NAMESPACE, - expires_in: default_ttl_seconds + expires_in: default_ttl_seconds, + error_handler: ::Gitlab::Redis::ERROR_HANDLER } end diff --git a/lib/gitlab/redis/rate_limiting.rb b/lib/gitlab/redis/rate_limiting.rb index 3a9fb63a495..12710bafbea 100644 --- a/lib/gitlab/redis/rate_limiting.rb +++ b/lib/gitlab/redis/rate_limiting.rb @@ -10,7 +10,11 @@ module Gitlab end def cache_store - @cache_store ||= ActiveSupport::Cache::RedisCacheStore.new(redis: pool, namespace: Cache::CACHE_NAMESPACE) + @cache_store ||= ActiveSupport::Cache::RedisCacheStore.new( + redis: pool, + namespace: Cache::CACHE_NAMESPACE, + error_handler: ::Gitlab::Redis::ERROR_HANDLER + ) end private diff --git a/lib/gitlab/redis/repository_cache.rb b/lib/gitlab/redis/repository_cache.rb index d8d434ae062..6c7bc8c41d5 100644 --- a/lib/gitlab/redis/repository_cache.rb +++ b/lib/gitlab/redis/repository_cache.rb @@ -14,7 +14,8 @@ module Gitlab redis: pool, compress: Gitlab::Utils.to_boolean(ENV.fetch('ENABLE_REDIS_CACHE_COMPRESSION', '1')), namespace: Cache::CACHE_NAMESPACE, - expires_in: Cache.default_ttl_seconds + expires_in: Cache.default_ttl_seconds, + error_handler: ::Gitlab::Redis::ERROR_HANDLER ) end end diff --git a/lib/gitlab/slash_commands/issue_move.rb b/lib/gitlab/slash_commands/issue_move.rb index 9f10da247d7..e42cdd0d433 100644 --- a/lib/gitlab/slash_commands/issue_move.rb +++ b/lib/gitlab/slash_commands/issue_move.rb @@ -29,7 +29,7 @@ module Gitlab return Gitlab::SlashCommands::Presenters::Access.new.not_found end - new_issue = ::Issues::MoveService.new(project: project, current_user: current_user) + new_issue = ::Issues::MoveService.new(container: project, current_user: current_user) .execute(old_issue, target_project) presenter(new_issue).present(old_issue) diff --git a/locale/gitlab.pot b/locale/gitlab.pot index db87d0353cd..ef899e10756 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -4107,6 +4107,9 @@ msgstr "" msgid "All Members" msgstr "" +msgid "All branch names must match %{link_start}this regular expression%{link_end}. If empty, any branch name is allowed." +msgstr "" + msgid "All branches" msgstr "" @@ -8740,6 +8743,9 @@ msgstr "" msgid "Choose which Git strategy to use when fetching the project." msgstr "" +msgid "Choose which branches should be mirrored" +msgstr "" + msgid "Choose which repositories you want to connect and run CI/CD pipelines." msgstr "" @@ -16773,6 +16779,9 @@ msgstr "" msgid "Exactly one of %{attributes} is required" msgstr "" +msgid "Example" +msgstr "" + msgid "Example: (feature|hotfix)\\/*" msgstr "" @@ -21088,6 +21097,9 @@ msgstr "" msgid "IdentityVerification|For added security, you'll need to verify your identity in a few quick steps." msgstr "" +msgid "IdentityVerification|For added security, you'll need to verify your identity." +msgstr "" + msgid "IdentityVerification|For added security, you'll need to verify your identity. We've sent a verification code to %{email}" msgstr "" @@ -21187,6 +21199,9 @@ msgstr "" msgid "IdentityVerification|We sent a new code to +%{phoneNumber}" msgstr "" +msgid "IdentityVerification|We've sent a verification code to %{email}" +msgstr "" + msgid "IdentityVerification|We've sent a verification code to +%{phoneNumber}" msgstr "" @@ -21235,6 +21250,9 @@ msgstr "" msgid "If disabled, only administrators can configure repository mirroring." msgstr "" +msgid "If enabled, all branches will be mirrored." +msgstr "" + msgid "If enabled, only protected branches will be mirrored." msgstr "" @@ -27329,6 +27347,12 @@ msgstr "" msgid "Minutes" msgstr "" +msgid "Mirror all branches" +msgstr "" + +msgid "Mirror branches" +msgstr "" + msgid "Mirror direction" msgstr "" @@ -27341,6 +27365,9 @@ msgstr "" msgid "Mirror settings are only available to GitLab administrators." msgstr "" +msgid "Mirror specific branches" +msgstr "" + msgid "Mirror user" msgstr "" @@ -29481,9 +29508,6 @@ msgstr "" msgid "OnDemandScans|Unable to fetch runner tags. Try reloading the page." msgstr "" -msgid "OnDemandScans|Verify" -msgstr "" - msgid "OnDemandScans|Verify configuration" msgstr "" @@ -38614,6 +38638,9 @@ msgstr "" msgid "SecurityReports|Activity" msgstr "" +msgid "SecurityReports|Add comment and dismiss" +msgstr "" + msgid "SecurityReports|Add or remove projects to monitor in the security area. Projects included in this list will have their results displayed in the security dashboard and vulnerability report." msgstr "" @@ -40781,6 +40808,9 @@ msgstr "" msgid "Spam log successfully submitted as ham." msgstr "" +msgid "Specific branches" +msgstr "" + msgid "Specified URL cannot be used: \"%{reason}\"" msgstr "" @@ -49362,6 +49392,9 @@ msgstr "" msgid "Your GitLab instance allows anyone to register for an account, which is a security risk on public-facing GitLab instances. You should deactivate new sign ups if public users aren't expected to register for an account." msgstr "" +msgid "Your GitLab version" +msgstr "" + msgid "Your Groups" msgstr "" diff --git a/spec/features/projects/settings/repository_settings_spec.rb b/spec/features/projects/settings/repository_settings_spec.rb index 6f0a3094849..a0625c93b1a 100644 --- a/spec/features/projects/settings/repository_settings_spec.rb +++ b/spec/features/projects/settings/repository_settings_spec.rb @@ -9,6 +9,7 @@ RSpec.describe 'Projects > Settings > Repository settings', feature_category: :p before do stub_feature_flags(branch_rules: false) + stub_feature_flags(mirror_only_branches_match_regex: false) project.add_role(user, role) sign_in(user) end diff --git a/spec/frontend/commit/mock_data.js b/spec/frontend/commit/mock_data.js index 80f62d5bfd8..a13ef9c563e 100644 --- a/spec/frontend/commit/mock_data.js +++ b/spec/frontend/commit/mock_data.js @@ -1,4 +1,4 @@ -const downstream = { +export const mockDownstreamPipelinesGraphql = ({ includeSourceJobRetried = true } = {}) => ({ nodes: [ { id: 'gid://gitlab/Ci::Pipeline/612', @@ -17,7 +17,7 @@ const downstream = { }, sourceJob: { id: 'gid://gitlab/Ci::Bridge/532', - retried: false, + retried: includeSourceJobRetried ? false : null, }, __typename: 'Pipeline', }, @@ -38,7 +38,7 @@ const downstream = { }, sourceJob: { id: 'gid://gitlab/Ci::Bridge/531', - retried: true, + retried: includeSourceJobRetried ? true : null, }, __typename: 'Pipeline', }, @@ -59,13 +59,13 @@ const downstream = { }, sourceJob: { id: 'gid://gitlab/Ci::Bridge/530', - retried: true, + retried: includeSourceJobRetried ? true : null, }, __typename: 'Pipeline', }, ], __typename: 'PipelineConnection', -}; +}); const upstream = { id: 'gid://gitlab/Ci::Pipeline/610', @@ -161,7 +161,7 @@ export const mockDownstreamQueryResponse = { pipeline: { path: '/root/ci-project/-/pipelines/790', id: 'pipeline-1', - downstream, + downstream: mockDownstreamPipelinesGraphql(), upstream: null, }, __typename: 'Project', @@ -176,7 +176,7 @@ export const mockUpstreamDownstreamQueryResponse = { pipeline: { id: 'pipeline-1', path: '/root/ci-project/-/pipelines/790', - downstream, + downstream: mockDownstreamPipelinesGraphql(), upstream, }, __typename: 'Project', diff --git a/spec/frontend/fixtures/pipelines.rb b/spec/frontend/fixtures/pipelines.rb index 44b471a70d8..768934d6278 100644 --- a/spec/frontend/fixtures/pipelines.rb +++ b/spec/frontend/fixtures/pipelines.rb @@ -23,8 +23,19 @@ RSpec.describe Projects::PipelinesController, '(JavaScript fixtures)', type: :co let!(:build_test) { create(:ci_build, pipeline: pipeline, stage: 'test') } let!(:build_deploy_failed) { create(:ci_build, status: :failed, pipeline: pipeline, stage: 'deploy') } + let(:bridge) { create(:ci_bridge, pipeline: pipeline) } + let(:retried_bridge) { create(:ci_bridge, :retried, pipeline: pipeline) } + + let(:downstream_pipeline) { create(:ci_pipeline, :with_job) } + let(:retried_downstream_pipeline) { create(:ci_pipeline, :with_job) } + let!(:ci_sources_pipeline) { create(:ci_sources_pipeline, pipeline: downstream_pipeline, source_job: bridge) } + let!(:retried_ci_sources_pipeline) do + create(:ci_sources_pipeline, pipeline: retried_downstream_pipeline, source_job: retried_bridge) + end + before do sign_in(user) + project.add_developer(user) end it 'pipelines/pipelines.json' do diff --git a/spec/frontend/issues/show/components/app_spec.js b/spec/frontend/issues/show/components/app_spec.js index 7766bc62026..9fa0ce6f93d 100644 --- a/spec/frontend/issues/show/components/app_spec.js +++ b/spec/frontend/issues/show/components/app_spec.js @@ -8,10 +8,10 @@ import waitForPromises from 'helpers/wait_for_promises'; import { createAlert } from '~/flash'; import { IssuableStatusText, - IssuableType, STATUS_CLOSED, STATUS_OPEN, STATUS_REOPENED, + TYPE_EPIC, TYPE_ISSUE, } from '~/issues/constants'; import IssuableApp from '~/issues/show/components/app.vue'; @@ -485,11 +485,11 @@ describe('Issuable output', () => { }); it.each` - issuableType | issuableStatus | statusIcon - ${TYPE_ISSUE} | ${STATUS_OPEN} | ${'issues'} - ${TYPE_ISSUE} | ${STATUS_CLOSED} | ${'issue-closed'} - ${IssuableType.Epic} | ${STATUS_OPEN} | ${'epic'} - ${IssuableType.Epic} | ${STATUS_CLOSED} | ${'epic-closed'} + issuableType | issuableStatus | statusIcon + ${TYPE_ISSUE} | ${STATUS_OPEN} | ${'issues'} + ${TYPE_ISSUE} | ${STATUS_CLOSED} | ${'issue-closed'} + ${TYPE_EPIC} | ${STATUS_OPEN} | ${'epic'} + ${TYPE_EPIC} | ${STATUS_CLOSED} | ${'epic-closed'} `( 'shows with state icon "$statusIcon" for $issuableType when status is $issuableStatus', async ({ issuableType, issuableStatus, statusIcon }) => { diff --git a/spec/frontend/issues/show/components/locked_warning_spec.js b/spec/frontend/issues/show/components/locked_warning_spec.js index c9d510088bf..dd3c7c58380 100644 --- a/spec/frontend/issues/show/components/locked_warning_spec.js +++ b/spec/frontend/issues/show/components/locked_warning_spec.js @@ -1,7 +1,7 @@ import { GlAlert, GlLink } from '@gitlab/ui'; import { mountExtended } from 'helpers/vue_test_utils_helper'; import { sprintf } from '~/locale'; -import { IssuableType, TYPE_ISSUE } from '~/issues/constants'; +import { TYPE_EPIC, TYPE_ISSUE } from '~/issues/constants'; import LockedWarning, { i18n } from '~/issues/show/components/locked_warning.vue'; describe('LockedWarning component', () => { @@ -21,7 +21,7 @@ describe('LockedWarning component', () => { const findAlert = () => wrapper.findComponent(GlAlert); const findLink = () => wrapper.findComponent(GlLink); - describe.each([TYPE_ISSUE, IssuableType.Epic])('with issuableType set to %s', (issuableType) => { + describe.each([TYPE_ISSUE, TYPE_EPIC])('with issuableType set to %s', (issuableType) => { let alert; let link; beforeEach(() => { diff --git a/spec/frontend/pipelines/pipelines_table_spec.js b/spec/frontend/pipelines/pipelines_table_spec.js index 9359bd9b95f..6ec8901038b 100644 --- a/spec/frontend/pipelines/pipelines_table_spec.js +++ b/spec/frontend/pipelines/pipelines_table_spec.js @@ -121,6 +121,14 @@ describe('Pipelines Table', () => { expect(findPipelineMiniGraph().props('stages').length).toBe(stagesLength); }); + it('should render the latest downstream pipelines only', () => { + // component receives two downstream pipelines. one of them is already outdated + // because we retried the trigger job, so the mini pipeline graph will only + // render the newly created downstream pipeline instead + expect(pipeline.triggered).toHaveLength(2); + expect(findPipelineMiniGraph().props('downstreamPipelines')).toHaveLength(1); + }); + describe('when pipeline does not have stages', () => { beforeEach(() => { pipeline = createMockPipeline(); diff --git a/spec/frontend/pipelines/utils_spec.js b/spec/frontend/pipelines/utils_spec.js index 1c23a7e4fcf..51e0e0705ff 100644 --- a/spec/frontend/pipelines/utils_spec.js +++ b/spec/frontend/pipelines/utils_spec.js @@ -3,6 +3,7 @@ import { makeLinksFromNodes, filterByAncestors, generateColumnsFromLayersListBare, + keepLatestDownstreamPipelines, listByLayers, parseData, removeOrphanNodes, @@ -10,6 +11,8 @@ import { } from '~/pipelines/components/parsing_utils'; import { createNodeDict } from '~/pipelines/utils'; +import { mockDownstreamPipelinesRest } from '../vue_merge_request_widget/mock_data'; +import { mockDownstreamPipelinesGraphql } from '../commit/mock_data'; import { mockParsedGraphQLNodes, missingJob } from './components/dag/mock_data'; import { generateResponse, mockPipelineResponse } from './graph/mock_data'; @@ -159,3 +162,37 @@ describe('DAG visualization parsing utilities', () => { }); }); }); + +describe('linked pipeline utilities', () => { + describe('keepLatestDownstreamPipelines', () => { + it('filters data from GraphQL', () => { + const downstream = mockDownstreamPipelinesGraphql().nodes; + const latestDownstream = keepLatestDownstreamPipelines(downstream); + + expect(downstream).toHaveLength(3); + expect(latestDownstream).toHaveLength(1); + }); + + it('filters data from REST', () => { + const downstream = mockDownstreamPipelinesRest(); + const latestDownstream = keepLatestDownstreamPipelines(downstream); + + expect(downstream).toHaveLength(2); + expect(latestDownstream).toHaveLength(1); + }); + + it('returns downstream pipelines if sourceJob.retried is null', () => { + const downstream = mockDownstreamPipelinesGraphql({ includeSourceJobRetried: false }).nodes; + const latestDownstream = keepLatestDownstreamPipelines(downstream); + + expect(latestDownstream).toHaveLength(downstream.length); + }); + + it('returns downstream pipelines if source_job.retried is null', () => { + const downstream = mockDownstreamPipelinesRest({ includeSourceJobRetried: false }); + const latestDownstream = keepLatestDownstreamPipelines(downstream); + + expect(latestDownstream).toHaveLength(downstream.length); + }); + }); +}); diff --git a/spec/frontend/sidebar/components/labels/labels_select_widget/labels_select_root_spec.js b/spec/frontend/sidebar/components/labels/labels_select_widget/labels_select_root_spec.js index cda562398c5..fd8e72bac49 100644 --- a/spec/frontend/sidebar/components/labels/labels_select_widget/labels_select_root_spec.js +++ b/spec/frontend/sidebar/components/labels/labels_select_widget/labels_select_root_spec.js @@ -4,7 +4,7 @@ import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import { createAlert } from '~/flash'; -import { IssuableType, TYPE_ISSUE } from '~/issues/constants'; +import { IssuableType, TYPE_EPIC, TYPE_ISSUE } from '~/issues/constants'; import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue'; import DropdownContents from '~/sidebar/components/labels/labels_select_widget/dropdown_contents.vue'; import DropdownValue from '~/sidebar/components/labels/labels_select_widget/dropdown_value.vue'; @@ -37,7 +37,7 @@ const errorQueryHandler = jest.fn().mockRejectedValue('Houston, we have a proble const updateLabelsMutation = { [TYPE_ISSUE]: updateIssueLabelsMutation, [IssuableType.MergeRequest]: updateMergeRequestLabelsMutation, - [IssuableType.Epic]: updateEpicLabelsMutation, + [TYPE_EPIC]: updateEpicLabelsMutation, [IssuableType.TestCase]: updateTestCaseLabelsMutation, }; @@ -215,7 +215,7 @@ describe('LabelsSelectRoot', () => { issuableType ${TYPE_ISSUE} ${IssuableType.MergeRequest} - ${IssuableType.Epic} + ${TYPE_EPIC} ${IssuableType.TestCase} `('when updating labels for $issuableType', ({ issuableType }) => { const label = { id: 'gid://gitlab/ProjectLabel/2' }; diff --git a/spec/frontend/super_sidebar/components/help_center_spec.js b/spec/frontend/super_sidebar/components/help_center_spec.js index 86d4286a3f7..bc847a3e159 100644 --- a/spec/frontend/super_sidebar/components/help_center_spec.js +++ b/spec/frontend/super_sidebar/components/help_center_spec.js @@ -14,6 +14,8 @@ jest.mock('~/whats_new'); describe('HelpCenter component', () => { let wrapper; + const GlEmoji = { template: '<img/>' }; + const findDropdownGroup = (i = 0) => { return wrapper.findAllComponents(GlDisclosureDropdownGroup).at(i); }; @@ -24,6 +26,7 @@ describe('HelpCenter component', () => { const createWrapper = (sidebarData) => { wrapper = mountExtended(HelpCenter, { propsData: { sidebarData }, + stubs: { GlEmoji }, }); }; @@ -52,6 +55,18 @@ describe('HelpCenter component', () => { ]); }); + describe('with Gitlab version check feature enabled', () => { + beforeEach(() => { + createWrapper({ ...sidebarData, show_version_check: true }); + }); + + it('shows version information as first item', () => { + expect(findDropdownGroup(0).props('group').items).toEqual([ + { text: HelpCenter.i18n.version, href: helpPagePath('update/index'), version: '16.0' }, + ]); + }); + }); + describe('showKeyboardShortcuts', () => { beforeEach(() => { jest.spyOn(wrapper.vm.$refs.dropdown, 'close'); diff --git a/spec/frontend/super_sidebar/mock_data.js b/spec/frontend/super_sidebar/mock_data.js index 8467306eb34..91a2dc93a47 100644 --- a/spec/frontend/super_sidebar/mock_data.js +++ b/spec/frontend/super_sidebar/mock_data.js @@ -71,4 +71,7 @@ export const sidebarData = { display_whats_new: true, whats_new_most_recent_release_items_count: 5, whats_new_version_digest: 1, + show_version_check: false, + gitlab_version: { major: 16, minor: 0 }, + gitlab_version_check: { severity: 'success' }, }; diff --git a/spec/frontend/vue_merge_request_widget/components/mr_widget_pipeline_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_pipeline_spec.js index 441054493f7..6a899c00b98 100644 --- a/spec/frontend/vue_merge_request_widget/components/mr_widget_pipeline_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_pipeline_spec.js @@ -111,6 +111,14 @@ describe('MRWidgetPipeline', () => { expect(findPipelineMiniGraph().props('stages')).toHaveLength(stagesCount); }); + it('should render the latest downstream pipelines only', () => { + // component receives two downstream pipelines. one of them is already outdated + // because we retried the trigger job, so the mini pipeline graph will only + // render the newly created downstream pipeline instead + expect(mockData.pipeline.triggered).toHaveLength(2); + expect(findPipelineMiniGraph().props('downstreamPipelines')).toHaveLength(1); + }); + describe('should render pipeline coverage information', () => { it('should render coverage percentage', () => { expect(findPipelineCoverage().text()).toMatch( diff --git a/spec/frontend/vue_merge_request_widget/components/report_widget_container_spec.js b/spec/frontend/vue_merge_request_widget/components/report_widget_container_spec.js index 2f6002f36e0..436f74d1be2 100644 --- a/spec/frontend/vue_merge_request_widget/components/report_widget_container_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/report_widget_container_spec.js @@ -19,7 +19,13 @@ describe('app/assets/javascripts/vue_merge_request_widget/components/report_widg expect(wrapper.isVisible()).toBe(false); }); - it('shows the container when children have no content', async () => { + it('hides the container when children has only empty spaces', async () => { + createComponent({ slot: `<span><b> <br/>\t\r\n</b></span> ` }); + await nextTick(); + expect(wrapper.isVisible()).toBe(false); + }); + + it('shows the container when a child has content', async () => { createComponent({ slot: `<span><b>test</b></span>` }); await nextTick(); expect(wrapper.isVisible()).toBe(true); diff --git a/spec/frontend/vue_merge_request_widget/mock_data.js b/spec/frontend/vue_merge_request_widget/mock_data.js index 20d00a116bb..46e1919b0ea 100644 --- a/spec/frontend/vue_merge_request_widget/mock_data.js +++ b/spec/frontend/vue_merge_request_widget/mock_data.js @@ -1,5 +1,94 @@ import { SUCCESS } from '~/vue_merge_request_widget/components/deployment/constants'; +export const mockDownstreamPipelinesRest = ({ includeSourceJobRetried = true } = {}) => [ + { + id: 632, + user: { + id: 1, + username: 'root', + name: 'Administrator', + state: 'active', + avatar_url: + 'https://secure.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + web_url: 'https://gdk.test:3000/root', + show_status: false, + path: '/root', + }, + active: false, + coverage: null, + source: 'parent_pipeline', + source_job: { + name: 'bridge_job', + retried: includeSourceJobRetried ? false : null, + }, + path: '/kitchen-sink/bakery/-/pipelines/632', + details: { + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + group: 'success', + tooltip: 'passed', + has_details: true, + details_path: '/kitchen-sink/bakery/-/pipelines/632', + illustration: null, + favicon: + '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png', + }, + }, + project: { + id: 21, + name: 'bakery', + full_path: '/kitchen-sink/bakery', + full_name: 'kitchen-sink / bakery', + refs_url: '/kitchen-sink/bakery/refs', + }, + }, + { + id: 633, + user: { + id: 1, + username: 'root', + name: 'Administrator', + state: 'active', + avatar_url: + 'https://secure.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + web_url: 'https://gdk.test:3000/root', + show_status: false, + path: '/root', + }, + active: false, + coverage: null, + source: 'parent_pipeline', + source_job: { + name: 'bridge_job', + retried: includeSourceJobRetried ? true : null, + }, + path: '/kitchen-sink/bakery/-/pipelines/633', + details: { + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + group: 'success', + tooltip: 'passed', + has_details: true, + details_path: '/kitchen-sink/bakery/-/pipelines/633', + illustration: null, + favicon: + '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png', + }, + }, + project: { + id: 21, + name: 'bakery', + full_path: '/kitchen-sink/bakery', + full_name: 'kitchen-sink / bakery', + refs_url: '/kitchen-sink/bakery/refs', + }, + }, +]; + export const artifacts = [ { text: 'result.txt', @@ -207,6 +296,7 @@ export default { retry_path: '/root/acets-app/pipelines/172/retry', created_at: '2017-04-07T12:27:19.520Z', updated_at: '2017-04-07T15:28:44.800Z', + triggered: mockDownstreamPipelinesRest(), }, pipelineCoverageDelta: '15.25', buildsWithCoverage: [ diff --git a/spec/frontend/vue_shared/components/confidentiality_badge_spec.js b/spec/frontend/vue_shared/components/confidentiality_badge_spec.js index 127869e99b9..3f7ec156c19 100644 --- a/spec/frontend/vue_shared/components/confidentiality_badge_spec.js +++ b/spec/frontend/vue_shared/components/confidentiality_badge_spec.js @@ -1,7 +1,7 @@ import { GlBadge } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; -import { WorkspaceType, IssuableType, TYPE_ISSUE } from '~/issues/constants'; +import { WorkspaceType, TYPE_ISSUE, TYPE_EPIC } from '~/issues/constants'; import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue'; @@ -28,9 +28,9 @@ describe('ConfidentialityBadge', () => { }); it.each` - workspaceType | issuableType | expectedTooltip - ${WorkspaceType.project} | ${TYPE_ISSUE} | ${'Only project members with at least the Reporter role, the author, and assignees can view or be notified about this issue.'} - ${WorkspaceType.group} | ${IssuableType.Epic} | ${'Only group members with at least the Reporter role can view or be notified about this epic.'} + workspaceType | issuableType | expectedTooltip + ${WorkspaceType.project} | ${TYPE_ISSUE} | ${'Only project members with at least the Reporter role, the author, and assignees can view or be notified about this issue.'} + ${WorkspaceType.group} | ${TYPE_EPIC} | ${'Only group members with at least the Reporter role can view or be notified about this epic.'} `( 'should render gl-badge with correct tooltip when workspaceType is $workspaceType and issuableType is $issuableType', ({ workspaceType, issuableType, expectedTooltip }) => { diff --git a/spec/helpers/sidebars_helper_spec.rb b/spec/helpers/sidebars_helper_spec.rb index e3e1d39b4f8..2b45f73815e 100644 --- a/spec/helpers/sidebars_helper_spec.rb +++ b/spec/helpers/sidebars_helper_spec.rb @@ -72,7 +72,10 @@ RSpec.describe SidebarsHelper do support_path: helper.support_url, display_whats_new: helper.display_whats_new?, whats_new_most_recent_release_items_count: helper.whats_new_most_recent_release_items_count, - whats_new_version_digest: helper.whats_new_version_digest + whats_new_version_digest: helper.whats_new_version_digest, + show_version_check: helper.show_version_check?, + gitlab_version: Gitlab.version_info, + gitlab_version_check: helper.gitlab_version_check }) end diff --git a/spec/helpers/web_hooks/web_hooks_helper_spec.rb b/spec/helpers/web_hooks/web_hooks_helper_spec.rb index bcd9d2df1dc..fdd0be8095b 100644 --- a/spec/helpers/web_hooks/web_hooks_helper_spec.rb +++ b/spec/helpers/web_hooks/web_hooks_helper_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe WebHooks::WebHooksHelper do +RSpec.describe WebHooks::WebHooksHelper, :clean_gitlab_redis_shared_state, feature_category: :integrations do let_it_be_with_reload(:project) { create(:project) } let(:current_user) { nil } @@ -43,20 +43,12 @@ RSpec.describe WebHooks::WebHooksHelper do expect(helper).to be_show_project_hook_failed_callout(project: project) end - it 'caches the DB calls until the TTL', :use_clean_rails_memory_store_caching, :request_store do - helper.show_project_hook_failed_callout?(project: project) - - travel_to((described_class::EXPIRY_TTL - 1.second).from_now) do - expect do - helper.show_project_hook_failed_callout?(project: project) - end.not_to exceed_query_limit(0) + it 'stores a value' do + Gitlab::Redis::SharedState.with do |redis| + expect(redis).to receive(:set).with(anything, 'true', ex: 1.hour) end - travel_to((described_class::EXPIRY_TTL + 1.second).from_now) do - expect do - helper.show_project_hook_failed_callout?(project: project) - end.to exceed_query_limit(0) - end + helper.show_project_hook_failed_callout?(project: project) end end diff --git a/spec/lib/gitlab/auth/o_auth/user_spec.rb b/spec/lib/gitlab/auth/o_auth/user_spec.rb index beeb3ca7011..a5cbad74829 100644 --- a/spec/lib/gitlab/auth/o_auth/user_spec.rb +++ b/spec/lib/gitlab/auth/o_auth/user_spec.rb @@ -418,25 +418,54 @@ RSpec.describe Gitlab::Auth::OAuth::User, feature_category: :authentication_and_ end context "and LDAP user has an account already" do - let!(:existing_user) { create(:omniauth_user, name: 'John Doe', email: 'john@example.com', extern_uid: dn, provider: 'ldapmain', username: 'john') } + context 'when sync_name is disabled' do + before do + allow(Gitlab.config.ldap).to receive(:sync_name).and_return(false) + end - it "adds the omniauth identity to the LDAP account" do - allow(Gitlab::Auth::Ldap::Person).to receive(:find_by_uid).and_return(ldap_user) + let!(:existing_user) { create(:omniauth_user, name: 'John Doe', email: 'john@example.com', extern_uid: dn, provider: 'ldapmain', username: 'john') } - oauth_user.save # rubocop:disable Rails/SaveBang + it "adds the omniauth identity to the LDAP account" do + allow(Gitlab::Auth::Ldap::Person).to receive(:find_by_uid).and_return(ldap_user) - expect(gl_user).to be_valid - expect(gl_user.username).to eql 'john' - expect(gl_user.name).to eql 'John Doe' - expect(gl_user.email).to eql 'john@example.com' - expect(gl_user.identities.length).to be 2 - identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } } - expect(identities_as_hash).to match_array( - [ - { provider: 'ldapmain', extern_uid: dn }, - { provider: 'twitter', extern_uid: uid } - ] - ) + oauth_user.save # rubocop:disable Rails/SaveBang + + expect(gl_user).to be_valid + expect(gl_user.username).to eql 'john' + expect(gl_user.name).to eql 'John Doe' + expect(gl_user.email).to eql 'john@example.com' + expect(gl_user.identities.length).to be 2 + identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } } + expect(identities_as_hash).to match_array( + [ + { provider: 'ldapmain', extern_uid: dn }, + { provider: 'twitter', extern_uid: uid } + ] + ) + end + end + + context 'when sync_name is enabled' do + let!(:existing_user) { create(:omniauth_user, name: 'John Swift', email: 'john@example.com', extern_uid: dn, provider: 'ldapmain', username: 'john') } + + it "adds the omniauth identity to the LDAP account" do + allow(Gitlab::Auth::Ldap::Person).to receive(:find_by_uid).and_return(ldap_user) + + oauth_user.save # rubocop:disable Rails/SaveBang + + expect(gl_user).to be_valid + expect(gl_user.username).to eql 'john' + expect(gl_user.name).to eql 'John Swift' + expect(gl_user.email).to eql 'john@example.com' + expect(gl_user.identities.length).to be 2 + identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } } + expect(identities_as_hash).to match_array( + [ + { provider: 'ldapmain', extern_uid: dn }, + { provider: 'twitter', extern_uid: uid } + ] + ) + end end end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index d6d0de63a3b..72043ba2a21 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -2106,16 +2106,22 @@ RSpec.describe Gitlab::Git::Repository, feature_category: :source_code_managemen let(:repository) { mutable_repository } let(:source_sha) { '913c66a37b4a45b9769037c55c2d238bd0942d2e' } let(:target_branch) { 'test-merge-target-branch' } + let(:target_sha) { '6d394385cf567f80a8fd85055db1ab4c5295806f' } before do - repository.create_branch(target_branch, '6d394385cf567f80a8fd85055db1ab4c5295806f') + repository.create_branch(target_branch, target_sha) end it 'can perform a merge' do merge_commit_id = nil - result = repository.merge(user, source_sha, target_branch, 'Test merge') do |commit_id| - merge_commit_id = commit_id - end + result = + repository.merge(user, + source_sha: source_sha, + target_branch: target_branch, + target_sha: target_sha, + message: 'Test merge') do |commit_id| + merge_commit_id = commit_id + end expect(result.newrev).to eq(merge_commit_id) expect(result.repo_created).to eq(false) @@ -2124,10 +2130,15 @@ RSpec.describe Gitlab::Git::Repository, feature_category: :source_code_managemen it 'returns nil if there was a concurrent branch update' do concurrent_update_id = '33f3729a45c02fc67d00adb1b8bca394b0e761d9' - result = repository.merge(user, source_sha, target_branch, 'Test merge') do - # This ref update should make the merge fail - repository.write_ref(Gitlab::Git::BRANCH_REF_PREFIX + target_branch, concurrent_update_id) - end + result = + repository.merge(user, + source_sha: source_sha, + target_branch: target_branch, + target_sha: target_sha, + message: 'Test merge') do |_commit_id| + # This ref update should make the merge fail + repository.write_ref(Gitlab::Git::BRANCH_REF_PREFIX + target_branch, concurrent_update_id) + end # This 'nil' signals that the merge was not applied expect(result).to be_nil @@ -2147,7 +2158,13 @@ RSpec.describe Gitlab::Git::Repository, feature_category: :source_code_managemen repository.create_branch(target_branch, branch_head) end - subject { repository.ff_merge(user, source_sha, target_branch) } + subject do + repository.ff_merge(user, + source_sha: source_sha, + target_branch: target_branch, + target_sha: branch_head + ) + end shared_examples '#ff_merge' do it 'performs a ff_merge' do @@ -2159,7 +2176,7 @@ RSpec.describe Gitlab::Git::Repository, feature_category: :source_code_managemen end context 'with a non-existing target branch' do - subject { repository.ff_merge(user, source_sha, 'this-isnt-real') } + subject { repository.ff_merge(user, source_sha: source_sha, target_branch: 'this-isnt-real') } it 'throws an ArgumentError' do expect { subject }.to raise_error(ArgumentError) @@ -2187,8 +2204,9 @@ RSpec.describe Gitlab::Git::Repository, feature_category: :source_code_managemen it "calls Gitaly's OperationService" do expect_any_instance_of(Gitlab::GitalyClient::OperationService) - .to receive(:user_ff_branch).with(user, source_sha, target_branch) - .and_return(nil) + .to receive(:user_ff_branch).with( + user, source_sha: source_sha, target_branch: target_branch, target_sha: branch_head + ).and_return(nil) subject end diff --git a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb index 443efd9828f..84672eb81c0 100644 --- a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::GitalyClient::OperationService do +RSpec.describe Gitlab::GitalyClient::OperationService, feature_category: :source_code_management do let_it_be(:user) { create(:user) } let_it_be(:project) { create(:project, :repository) } @@ -279,12 +279,39 @@ RSpec.describe Gitlab::GitalyClient::OperationService do describe '#user_merge_branch' do let(:target_branch) { 'master' } + let(:target_sha) { repository.commit(target_branch).sha } let(:source_sha) { '5937ac0a7beb003549fc5fd26fc247adbce4a52e' } let(:message) { 'Merge a branch' } - subject { client.user_merge_branch(user, source_sha, target_branch, message) {} } + subject do + client.user_merge_branch(user, + source_sha: source_sha, + target_branch: target_branch, + target_sha: target_sha, + message: message + ) {} + end + + it 'sends a user_merge_branch message', :freeze_time do + first_request = + Gitaly::UserMergeBranchRequest.new( + repository: repository.gitaly_repository, + user: gitaly_user, + commit_id: source_sha, + branch: target_branch, + expected_old_oid: target_sha, + message: message, + timestamp: Google::Protobuf::Timestamp.new(seconds: Time.now.utc.to_i) + ) + + second_request = Gitaly::UserMergeBranchRequest.new(apply: true) + + expect_next_instance_of(Gitlab::GitalyClient::QueueEnumerator) do |instance| + expect(instance).to receive(:push).with(first_request).and_call_original + expect(instance).to receive(:push).with(second_request).and_call_original + expect(instance).to receive(:close) + end - it 'sends a user_merge_branch message' do expect(subject).to be_a(Gitlab::Git::OperationService::BranchUpdate) expect(subject.newrev).to be_present expect(subject.repo_created).to be(false) @@ -431,12 +458,14 @@ RSpec.describe Gitlab::GitalyClient::OperationService do describe '#user_ff_branch' do let(:target_branch) { 'my-branch' } + let(:target_sha) { '6d394385cf567f80a8fd85055db1ab4c5295806f' } let(:source_sha) { 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660' } let(:request) do Gitaly::UserFFBranchRequest.new( repository: repository.gitaly_repository, branch: target_branch, commit_id: source_sha, + expected_old_oid: target_sha, user: gitaly_user ) end @@ -457,7 +486,13 @@ RSpec.describe Gitlab::GitalyClient::OperationService do .and_return(response) end - subject { client.user_ff_branch(user, source_sha, target_branch) } + subject do + client.user_ff_branch(user, + source_sha: source_sha, + target_branch: target_branch, + target_sha: target_sha + ) + end it 'sends a user_ff_branch message and returns a BranchUpdate object' do expect(subject).to be_a(Gitlab::Git::OperationService::BranchUpdate) diff --git a/spec/lib/gitlab/redis/cache_spec.rb b/spec/lib/gitlab/redis/cache_spec.rb index 82ff8a26199..64615c4d9ad 100644 --- a/spec/lib/gitlab/redis/cache_spec.rb +++ b/spec/lib/gitlab/redis/cache_spec.rb @@ -26,5 +26,22 @@ RSpec.describe Gitlab::Redis::Cache do expect(described_class.active_support_config[:expires_in]).to eq(1.day) end + + context 'when encountering an error' do + let(:cache) { ActiveSupport::Cache::RedisCacheStore.new(**described_class.active_support_config) } + + subject { cache.read('x') } + + before do + described_class.with do |redis| + allow(redis).to receive(:get).and_raise(::Redis::CommandError) + end + end + + it 'logs error' do + expect(::Gitlab::ErrorTracking).to receive(:log_exception) + subject + end + end end end diff --git a/spec/lib/gitlab/redis/rate_limiting_spec.rb b/spec/lib/gitlab/redis/rate_limiting_spec.rb index e79c070df93..d82228426f0 100644 --- a/spec/lib/gitlab/redis/rate_limiting_spec.rb +++ b/spec/lib/gitlab/redis/rate_limiting_spec.rb @@ -4,4 +4,21 @@ require 'spec_helper' RSpec.describe Gitlab::Redis::RateLimiting do include_examples "redis_new_instance_shared_examples", 'rate_limiting', Gitlab::Redis::Cache + + describe '.cache_store' do + context 'when encountering an error' do + subject { described_class.cache_store.read('x') } + + before do + described_class.with do |redis| + allow(redis).to receive(:get).and_raise(::Redis::CommandError) + end + end + + it 'logs error' do + expect(::Gitlab::ErrorTracking).to receive(:log_exception) + subject + end + end + end end diff --git a/spec/lib/gitlab/redis/repository_cache_spec.rb b/spec/lib/gitlab/redis/repository_cache_spec.rb index 8cdc4580f9e..2c167a6eb62 100644 --- a/spec/lib/gitlab/redis/repository_cache_spec.rb +++ b/spec/lib/gitlab/redis/repository_cache_spec.rb @@ -17,5 +17,20 @@ RSpec.describe Gitlab::Redis::RepositoryCache, feature_category: :scalability do it 'has a default ttl of 8 hours' do expect(described_class.cache_store.options[:expires_in]).to eq(8.hours) end + + context 'when encountering an error' do + subject { described_class.cache_store.read('x') } + + before do + described_class.with do |redis| + allow(redis).to receive(:get).and_raise(::Redis::CommandError) + end + end + + it 'logs error' do + expect(::Gitlab::ErrorTracking).to receive(:log_exception) + subject + end + end end end diff --git a/spec/lib/gitlab/slash_commands/presenters/issue_move_spec.rb b/spec/lib/gitlab/slash_commands/presenters/issue_move_spec.rb index 7d36e67ddbf..15df2cea909 100644 --- a/spec/lib/gitlab/slash_commands/presenters/issue_move_spec.rb +++ b/spec/lib/gitlab/slash_commands/presenters/issue_move_spec.rb @@ -8,7 +8,7 @@ RSpec.describe Gitlab::SlashCommands::Presenters::IssueMove do let_it_be(:other_project) { create(:project) } let_it_be(:old_issue, reload: true) { create(:issue, project: project) } - let(:new_issue) { Issues::MoveService.new(project: project, current_user: user).execute(old_issue, other_project) } + let(:new_issue) { Issues::MoveService.new(container: project, current_user: user).execute(old_issue, other_project) } let(:attachment) { subject[:attachments].first } subject { described_class.new(new_issue).present(old_issue) } diff --git a/spec/migrations/recount_epic_cache_counts_v3_spec.rb b/spec/migrations/recount_epic_cache_counts_v3_spec.rb new file mode 100644 index 00000000000..24b89ab30ca --- /dev/null +++ b/spec/migrations/recount_epic_cache_counts_v3_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration! + +RSpec.describe RecountEpicCacheCountsV3, :migration, feature_category: :portfolio_management do + let(:migration) { described_class::MIGRATION } + + describe '#up' do + it 'schedules a batched background migration' do + migrate! + + expect(migration).to have_scheduled_batched_migration( + table_name: :epics, + column_name: :id, + interval: described_class::DELAY_INTERVAL, + batch_size: described_class::BATCH_SIZE, + max_batch_size: described_class::MAX_BATCH_SIZE, + sub_batch_size: described_class::SUB_BATCH_SIZE + ) + end + end + + describe '#down' do + it 'deletes all batched migration records' do + migrate! + schema_migrate_down! + + expect(migration).not_to have_scheduled_batched_migration + end + end +end diff --git a/spec/models/hooks/project_hook_spec.rb b/spec/models/hooks/project_hook_spec.rb index 9a78d7b1719..c3484c4a42c 100644 --- a/spec/models/hooks/project_hook_spec.rb +++ b/spec/models/hooks/project_hook_spec.rb @@ -79,17 +79,91 @@ RSpec.describe ProjectHook, feature_category: :integrations do end describe '#update_last_failure', :clean_gitlab_redis_shared_state do - let(:hook) { build(:project_hook) } + let_it_be(:hook) { create(:project_hook) } + + def last_failure + Gitlab::Redis::SharedState.with do |redis| + redis.get(hook.project.last_failure_redis_key) + end + end + + def any_failed? + Gitlab::Redis::SharedState.with do |redis| + Gitlab::Utils.to_boolean(redis.get(hook.project.web_hook_failure_redis_key)) + end + end it 'is a method of this class' do expect { hook.update_last_failure }.not_to raise_error end context 'when the hook is executable' do - it 'does not update the state' do - expect(Gitlab::Redis::SharedState).not_to receive(:with) + let(:redis_key) { hook.project.web_hook_failure_redis_key } + + def redis_value + any_failed? + end + + context 'when the state was previously failing' do + before do + Gitlab::Redis::SharedState.with do |redis| + redis.set(redis_key, true) + end + end - hook.update_last_failure + it 'does update the state' do + expect { hook.update_last_failure }.to change { redis_value }.to(false) + end + + context 'when there is another failing sibling hook' do + before do + create(:project_hook, :permanently_disabled, project: hook.project) + end + + it 'does not update the state' do + expect { hook.update_last_failure }.not_to change { redis_value }.from(true) + end + + it 'caches the current value' do + Gitlab::Redis::SharedState.with do |redis| + expect(redis).to receive(:set).with(redis_key, 'true', ex: 1.hour).and_call_original + end + + hook.update_last_failure + end + end + end + + context 'when the state was previously unknown' do + before do + Gitlab::Redis::SharedState.with do |redis| + redis.del(redis_key) + end + end + + it 'does not update the state' do + expect { hook.update_last_failure }.not_to change { redis_value }.from(nil) + end + end + + context 'when the state was previously not failing' do + before do + Gitlab::Redis::SharedState.with do |redis| + redis.set(redis_key, false) + end + end + + it 'does not update the state' do + expect { hook.update_last_failure }.not_to change { redis_value }.from(false) + end + + it 'does not cache the current value' do + Gitlab::Redis::SharedState.with do |redis| + expect(redis).not_to receive(:set) + end + + hook.update_last_failure + end end end @@ -98,28 +172,34 @@ RSpec.describe ProjectHook, feature_category: :integrations do allow(hook).to receive(:executable?).and_return(false) end - def last_failure - Gitlab::Redis::SharedState.with do |redis| - redis.get("web_hooks:last_failure:project-#{hook.project.id}") - end - end - context 'there is no prior value', :freeze_time do - it 'updates the state' do + it 'updates last_failure' do expect { hook.update_last_failure }.to change { last_failure }.to(Time.current) end + + it 'updates any_failed?' do + expect { hook.update_last_failure }.to change { any_failed? }.to(true) + end end - context 'there is a prior value, from before now' do + context 'when there is a prior last_failure, from before now' do it 'updates the state' do the_future = 1.minute.from_now - hook.update_last_failure travel_to(the_future) do expect { hook.update_last_failure }.to change { last_failure }.to(the_future.iso8601) end end + + it 'does not change the failing state' do + the_future = 1.minute.from_now + hook.update_last_failure + + travel_to(the_future) do + expect { hook.update_last_failure }.not_to change { any_failed? }.from(true) + end + end end context 'there is a prior value, from after now' do diff --git a/spec/models/hooks/web_hook_spec.rb b/spec/models/hooks/web_hook_spec.rb index 4bfa3328037..72958a54e10 100644 --- a/spec/models/hooks/web_hook_spec.rb +++ b/spec/models/hooks/web_hook_spec.rb @@ -443,7 +443,7 @@ RSpec.describe WebHook, feature_category: :integrations do describe '#update_last_failure' do it 'is a method of this class' do - expect { described_class.new.update_last_failure }.not_to raise_error + expect { described_class.new(project: project).update_last_failure }.not_to raise_error end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index c0af71a7e27..23dae7d0374 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -8826,6 +8826,14 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do end end + it_behaves_like 'something that has web-hooks' do + let_it_be_with_reload(:object) { create(:project) } + + def create_hook + create(:project_hook, project: object) + end + end + private def finish_job(export_job) diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index f3a8a9cded3..b8780b3faae 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -1848,6 +1848,8 @@ RSpec.describe Repository, feature_category: :source_code_management do end describe '#expire_root_ref_cache' do + let(:project) { create(:project) } + it 'expires the root reference cache' do repository.root_ref @@ -1949,6 +1951,40 @@ RSpec.describe Repository, feature_category: :source_code_management do end end + describe '#merge_to_branch' do + let(:merge_request) do + create(:merge_request, source_branch: 'feature', target_branch: project.default_branch, source_project: project) + end + + it 'merges two branches and returns the merge commit id' do + message = 'New merge commit' + merge_commit_id = + repository.merge_to_branch(user, + source_sha: merge_request.diff_head_sha, + target_branch: merge_request.target_branch, + target_sha: repository.commit(merge_request.target_branch).sha, + message: message) + + expect(repository.commit(merge_commit_id).message).to eq(message) + expect(repository.commit(merge_request.target_branch).sha).to eq(merge_commit_id) + end + + it 'does not merge if target branch has been changed' do + target_sha = project.commit.sha + + repository.create_file(user, 'file.txt', 'CONTENT', message: 'Add file', branch_name: project.default_branch) + + merge_commit_id = + repository.merge_to_branch(user, + source_sha: merge_request.diff_head_sha, + target_branch: merge_request.target_branch, + target_sha: target_sha, + message: 'New merge commit') + + expect(merge_commit_id).to be_nil + end + end + describe '#merge_to_ref' do let(:merge_request) do create(:merge_request, source_branch: 'feature', @@ -1975,15 +2011,20 @@ RSpec.describe Repository, feature_category: :source_code_management do end describe '#ff_merge' do + let(:target_branch) { 'ff-target' } + let(:merge_request) do + create(:merge_request, source_branch: 'feature', target_branch: target_branch, source_project: project) + end + before do - repository.add_branch(user, 'ff-target', 'feature~5') + repository.add_branch(user, target_branch, 'feature~5') end it 'merges the code and return the commit id' do - merge_request = create(:merge_request, source_branch: 'feature', target_branch: 'ff-target', source_project: project) merge_commit_id = repository.ff_merge(user, merge_request.diff_head_sha, merge_request.target_branch, + target_sha: repository.commit(merge_request.target_branch).sha, merge_request: merge_request) merge_commit = repository.commit(merge_commit_id) @@ -1992,14 +2033,24 @@ RSpec.describe Repository, feature_category: :source_code_management do end it 'sets the `in_progress_merge_commit_sha` flag for the given merge request' do - merge_request = create(:merge_request, source_branch: 'feature', target_branch: 'ff-target', source_project: project) merge_commit_id = repository.ff_merge(user, merge_request.diff_head_sha, merge_request.target_branch, + target_sha: repository.commit(merge_request.target_branch).sha, merge_request: merge_request) expect(merge_request.in_progress_merge_commit_sha).to eq(merge_commit_id) end + + it 'does not merge if target branch has been changed' do + target_sha = project.commit(target_branch).sha + + repository.create_file(user, 'file.txt', 'CONTENT', message: 'Add file', branch_name: target_branch) + + merge_commit_id = repository.ff_merge(user, merge_request.diff_head_sha, target_branch, target_sha: target_sha) + + expect(merge_commit_id).to be_nil + end end describe '#rebase' do diff --git a/spec/requests/api/graphql/issue/issue_spec.rb b/spec/requests/api/graphql/issue/issue_spec.rb index 101de692aa5..3665fbc2df8 100644 --- a/spec/requests/api/graphql/issue/issue_spec.rb +++ b/spec/requests/api/graphql/issue/issue_spec.rb @@ -154,6 +154,47 @@ RSpec.describe 'Query.issue(id)', feature_category: :team_planning do end end + context 'when selecting `related_merge_requests`' do + let(:issue_fields) { ['relatedMergeRequests { nodes { id } }'] } + let_it_be(:user) { create(:user) } + let_it_be(:mr_project) { project } + let!(:merge_request) do + attributes = { + author: user, + source_project: mr_project, + target_project: mr_project, + source_branch: 'master', + target_branch: 'test', + description: "See #{issue.to_reference}" + } + + create(:merge_request, attributes).tap do |merge_request| + create(:note, :system, project: issue.project, noteable: issue, + author: user, note: merge_request.to_reference(full: true)) + end + end + + before do + project.add_developer(current_user) + + post_graphql(query, current_user: current_user) + end + + it 'returns the related merge request' do + expect(issue_data['relatedMergeRequests']['nodes']).to include a_hash_including({ + 'id' => merge_request.to_global_id.to_s + }) + end + + context 'no permission to related merge request' do + let_it_be(:mr_project) { create(:project, :private) } + + it 'does not return the related merge request' do + expect(issue_data['relatedMergeRequests']['nodes']).to be_empty + end + end + end + context 'when there is a confidential issue' do let!(:confidential_issue) do create(:issue, :confidential, project: project) diff --git a/spec/requests/api/graphql/project/alert_management/alerts_spec.rb b/spec/requests/api/graphql/project/alert_management/alerts_spec.rb index 304edfbf4e4..55d223daf27 100644 --- a/spec/requests/api/graphql/project/alert_management/alerts_spec.rb +++ b/spec/requests/api/graphql/project/alert_management/alerts_spec.rb @@ -16,7 +16,7 @@ RSpec.describe 'getting Alert Management Alerts', feature_category: :incident_ma let(:fields) do <<~QUERY nodes { - #{all_graphql_fields_for('AlertManagementAlert', excluded: ['assignees'])} + #{all_graphql_fields_for('AlertManagementAlert', excluded: %w[assignees relatedMergeRequests])} } QUERY end diff --git a/spec/serializers/issue_entity_spec.rb b/spec/serializers/issue_entity_spec.rb index 4bb1e00fa18..cc3635a0afc 100644 --- a/spec/serializers/issue_entity_spec.rb +++ b/spec/serializers/issue_entity_spec.rb @@ -65,7 +65,7 @@ RSpec.describe IssueEntity do before do project.add_developer(member) public_project.add_developer(member) - Issues::MoveService.new(project: public_project, current_user: member).execute(issue, project) + Issues::MoveService.new(container: public_project, current_user: member).execute(issue, project) end context 'when user cannot read target project' do diff --git a/spec/serializers/pipeline_details_entity_spec.rb b/spec/serializers/pipeline_details_entity_spec.rb index 52a01787711..de05d2afd2b 100644 --- a/spec/serializers/pipeline_details_entity_spec.rb +++ b/spec/serializers/pipeline_details_entity_spec.rb @@ -182,6 +182,7 @@ RSpec.describe PipelineDetailsEntity do expect(source_jobs[cross_project_pipeline.id][:name]).to eq('cross-project') expect(source_jobs[child_pipeline.id][:name]).to eq('child') + expect(source_jobs[child_pipeline.id][:retried]).to eq false end end end diff --git a/spec/services/issuable/destroy_service_spec.rb b/spec/services/issuable/destroy_service_spec.rb index c72d48d5b77..29f548e1c47 100644 --- a/spec/services/issuable/destroy_service_spec.rb +++ b/spec/services/issuable/destroy_service_spec.rb @@ -7,7 +7,7 @@ RSpec.describe Issuable::DestroyService do let(:group) { create(:group, :public) } let(:project) { create(:project, :public, group: group) } - subject(:service) { described_class.new(project: project, current_user: user) } + subject(:service) { described_class.new(container: project, current_user: user) } describe '#execute' do context 'when issuable is an issue' do diff --git a/spec/services/issues/clone_service_spec.rb b/spec/services/issues/clone_service_spec.rb index 67f32b85350..eafaea93015 100644 --- a/spec/services/issues/clone_service_spec.rb +++ b/spec/services/issues/clone_service_spec.rb @@ -22,7 +22,7 @@ RSpec.describe Issues::CloneService do let(:with_notes) { false } subject(:clone_service) do - described_class.new(project: old_project, current_user: user) + described_class.new(container: old_project, current_user: user) end shared_context 'user can clone issue' do diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb index 324b2aa9fe2..11f4d6a9ed4 100644 --- a/spec/services/issues/move_service_spec.rb +++ b/spec/services/issues/move_service_spec.rb @@ -20,7 +20,7 @@ RSpec.describe Issues::MoveService do end subject(:move_service) do - described_class.new(project: old_project, current_user: user) + described_class.new(container: old_project, current_user: user) end shared_context 'user can move issue' do diff --git a/spec/services/tasks_to_be_done/base_service_spec.rb b/spec/services/tasks_to_be_done/base_service_spec.rb index bf6be6d46e5..7219dae60be 100644 --- a/spec/services/tasks_to_be_done/base_service_spec.rb +++ b/spec/services/tasks_to_be_done/base_service_spec.rb @@ -18,7 +18,7 @@ RSpec.describe TasksToBeDone::BaseService do subject(:service) do TasksToBeDone::CreateCiTaskService.new( - project: project, + container: project, current_user: current_user, assignee_ids: assignee_ids ) diff --git a/spec/services/work_items/delete_service_spec.rb b/spec/services/work_items/delete_service_spec.rb index 6cca5018852..69ae881a12f 100644 --- a/spec/services/work_items/delete_service_spec.rb +++ b/spec/services/work_items/delete_service_spec.rb @@ -16,7 +16,7 @@ RSpec.describe WorkItems::DeleteService do end describe '#execute' do - subject(:result) { described_class.new(project: project, current_user: user).execute(work_item) } + subject(:result) { described_class.new(container: project, current_user: user).execute(work_item) } context 'when user can delete the work item' do it { is_expected.to be_success } diff --git a/spec/support/shared_examples/models/concerns/web_hooks/has_web_hooks_shared_examples.rb b/spec/support/shared_examples/models/concerns/web_hooks/has_web_hooks_shared_examples.rb new file mode 100644 index 00000000000..cd6eb8c77fa --- /dev/null +++ b/spec/support/shared_examples/models/concerns/web_hooks/has_web_hooks_shared_examples.rb @@ -0,0 +1,107 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'something that has web-hooks' do + describe '#any_hook_failed?', :clean_gitlab_redis_shared_state do + subject { object.any_hook_failed? } + + context 'when there are no hooks' do + it { is_expected.to eq(false) } + end + + context 'when there are hooks' do + before do + create_hook + create_hook + end + + it { is_expected.to eq(false) } + + context 'when there is a failed hook' do + before do + hook = create_hook + hook.update!(recent_failures: WebHook::FAILURE_THRESHOLD + 1) + end + + it { is_expected.to eq(true) } + end + end + end + + describe '#cache_web_hook_failure', :clean_gitlab_redis_shared_state do + context 'when no value is passed' do + it 'stores the return value of #any_hook_failed? and passes it back' do + allow(object).to receive(:any_hook_failed?).and_return(true) + + Gitlab::Redis::SharedState.with do |r| + expect(r).to receive(:set) + .with(object.web_hook_failure_redis_key, 'true', ex: 1.hour) + .and_call_original + end + + expect(object.cache_web_hook_failure).to eq(true) + end + end + + context 'when a value is passed' do + it 'stores the value and passes it back' do + expect(object).not_to receive(:any_hook_failed?) + + Gitlab::Redis::SharedState.with do |r| + expect(r).to receive(:set) + .with(object.web_hook_failure_redis_key, 'foo', ex: 1.hour) + .and_call_original + end + + expect(object.cache_web_hook_failure(:foo)).to eq(:foo) + end + end + end + + describe '#get_web_hook_failure', :clean_gitlab_redis_shared_state do + subject { object.get_web_hook_failure } + + context 'when no value is stored' do + it { is_expected.to be_nil } + end + + context 'when stored as true' do + before do + object.cache_web_hook_failure(true) + end + + it { is_expected.to eq(true) } + end + + context 'when stored as false' do + before do + object.cache_web_hook_failure(false) + end + + it { is_expected.to eq(false) } + end + end + + describe '#fetch_web_hook_failure', :clean_gitlab_redis_shared_state do + context 'when a value has not been stored' do + it 'does not call #any_hook_failed?' do + expect(object.get_web_hook_failure).to be_nil + expect(object).to receive(:any_hook_failed?).and_return(true) + + expect(object.fetch_web_hook_failure).to eq(true) + expect(object.get_web_hook_failure).to eq(true) + end + end + + context 'when a value has been stored' do + before do + object.cache_web_hook_failure(true) + end + + it 'does not call #any_hook_failed?' do + expect(object).not_to receive(:any_hook_failed?) + + expect(object.fetch_web_hook_failure).to eq(true) + end + end + end +end diff --git a/spec/support/shared_examples/requests/api/graphql/issue_list_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/issue_list_shared_examples.rb index d4af9e570d1..6c8b792bf92 100644 --- a/spec/support/shared_examples/requests/api/graphql/issue_list_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/graphql/issue_list_shared_examples.rb @@ -5,7 +5,7 @@ RSpec.shared_examples 'graphql issue list request spec' do let(:fields) do <<~QUERY nodes { - #{all_graphql_fields_for('issues'.classify)} + #{all_graphql_fields_for('issues'.classify, excluded: ['relatedMergeRequests'])} } QUERY end @@ -683,6 +683,28 @@ RSpec.shared_examples 'graphql issue list request spec' do end end + context 'when selecting `related_merge_requests`' do + let(:fields) do + <<~QUERY + nodes { + relatedMergeRequests { + nodes { + id + } + } + } + QUERY + end + + it 'limits the field to 1 execution' do + post_query + + expect_graphql_errors_to_include( + '"relatedMergeRequests" field can be requested only for 1 Issue(s) at a time.' + ) + end + end + it 'includes a web_url' do post_query diff --git a/spec/workers/tasks_to_be_done/create_worker_spec.rb b/spec/workers/tasks_to_be_done/create_worker_spec.rb index e884a71933e..c3c3612f9a7 100644 --- a/spec/workers/tasks_to_be_done/create_worker_spec.rb +++ b/spec/workers/tasks_to_be_done/create_worker_spec.rb @@ -20,7 +20,7 @@ RSpec.describe TasksToBeDone::CreateWorker do expect(service_class) .to receive(:new) - .with(project: member_task.project, current_user: current_user, assignee_ids: assignee_ids) + .with(container: member_task.project, current_user: current_user, assignee_ids: assignee_ids) .and_call_original end |