diff options
21 files changed, 289 insertions, 175 deletions
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index dd7914a7999..dd2a5654d2c 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -9d00c51754d1199119e46d68db3b233115118afa +0fed39f4d44b51ff5dfa20e6dc4a1a940a5a6b38 diff --git a/app/assets/javascripts/integrations/gitlab_slack_application/components/gitlab_slack_application.vue b/app/assets/javascripts/integrations/gitlab_slack_application/components/gitlab_slack_application.vue index bcb199853bd..edfb0af5bbe 100644 --- a/app/assets/javascripts/integrations/gitlab_slack_application/components/gitlab_slack_application.vue +++ b/app/assets/javascripts/integrations/gitlab_slack_application/components/gitlab_slack_application.vue @@ -101,7 +101,7 @@ export default { @project-selected="selectProject" /> - <div class="gl-display-flex gl-justify-content-end"> + <div class="gl-display-flex gl-justify-content-end gl-mt-3"> <gl-button category="primary" variant="confirm" diff --git a/app/assets/javascripts/integrations/gitlab_slack_application/components/projects_dropdown.vue b/app/assets/javascripts/integrations/gitlab_slack_application/components/projects_dropdown.vue index 26d191cd0bf..7c5287c69d6 100644 --- a/app/assets/javascripts/integrations/gitlab_slack_application/components/projects_dropdown.vue +++ b/app/assets/javascripts/integrations/gitlab_slack_application/components/projects_dropdown.vue @@ -1,13 +1,13 @@ <script> -import { GlDropdown } from '@gitlab/ui'; +import { GlCollapsibleListbox, GlAvatarLabeled } from '@gitlab/ui'; import { __ } from '~/locale'; - -import ProjectListItem from '~/vue_shared/components/project_selector/project_list_item.vue'; +import { getIdFromGraphQLId, isGid } from '~/graphql_shared/utils'; +import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; export default { components: { - GlDropdown, - ProjectListItem, + GlCollapsibleListbox, + GlAvatarLabeled, }, props: { projectDropdownText: { @@ -26,30 +26,61 @@ export default { default: null, }, }, + data() { + return { + selected: this.selectedProject, + }; + }, computed: { dropdownText() { return this.selectedProject ? this.selectedProject.name_with_namespace : this.projectDropdownText; }, + items() { + const items = this.projects.map((project) => { + return { + value: project.id, + ...project, + }; + }); + + return convertObjectPropsToCamelCase(items, { deep: true }); + }, }, methods: { - onClick(project) { - this.$emit('project-selected', project); - this.$refs.dropdown.hide(true); + getEntityId(project) { + return isGid(project.id) ? getIdFromGraphQLId(project.id) : project.id; + }, + selectProject(projectId) { + this.$emit( + 'project-selected', + this.projects.find((project) => project.id === projectId), + ); }, }, }; </script> <template> - <gl-dropdown ref="dropdown" block :text="dropdownText" menu-class="gl-w-full!"> - <project-list-item - v-for="project in projects" - :key="project.id" - :project="project" - :selected="false" - @click="onClick(project)" - /> - </gl-dropdown> + <gl-collapsible-listbox + v-model="selected" + block + fluid-width + is-check-centered + :toggle-text="dropdownText" + :items="items" + @select="selectProject" + > + <template #list-item="{ item }"> + <gl-avatar-labeled + :label="item.nameWithNamespace" + :entity-name="item.nameWithNamespace" + :entity-id="getEntityId(item)" + shape="rect" + :size="32" + :src="item.avatarUrl" + /> + </template> + </gl-collapsible-listbox> </template> diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 6a72ed6476e..65264596978 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -2115,6 +2115,18 @@ class MergeRequest < ApplicationRecord !squash && target_project.squash_always? end + def current_patch_id_sha + return merge_request_diff.patch_id_sha if merge_request_diff.patch_id_sha.present? + + base_sha = diff_refs&.base_sha + head_sha = diff_refs&.head_sha + + return unless base_sha && head_sha + return if base_sha == head_sha + + project.repository.get_patch_id(base_sha, head_sha) + end + private attr_accessor :skip_fetch_ref diff --git a/app/services/merge_requests/approval_service.rb b/app/services/merge_requests/approval_service.rb index dbe5567cbc5..f9857cdad39 100644 --- a/app/services/merge_requests/approval_service.rb +++ b/app/services/merge_requests/approval_service.rb @@ -7,7 +7,7 @@ module MergeRequests approval = merge_request.approvals.new( user: current_user, - patch_id_sha: fetch_patch_id_sha(merge_request) + patch_id_sha: merge_request.current_patch_id_sha ) return success unless save_approval(approval) @@ -36,17 +36,6 @@ module MergeRequests private - def fetch_patch_id_sha(merge_request) - diff_refs = merge_request.diff_refs - base_sha = diff_refs&.base_sha - head_sha = diff_refs&.head_sha - - return unless base_sha && head_sha - return if base_sha == head_sha - - merge_request.project.repository.get_patch_id(base_sha, head_sha) - end - def eligible_for_approval?(merge_request) merge_request.eligible_for_approval_by?(current_user) end diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 6871aaada12..428c0830d83 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -80,7 +80,7 @@ WARNING: **Introduced** in 16.1. This feature is an Experiment. It can be changed or removed at any time. -Returns [`AiChatMessageConnection!`](#aichatmessageconnection). +Returns [`AiMessageConnection!`](#aimessageconnection). This field returns a [connection](#connections). It accepts the four standard [pagination arguments](#connection-pagination-arguments): @@ -91,7 +91,7 @@ four standard [pagination arguments](#connection-pagination-arguments): | Name | Type | Description | | ---- | ---- | ----------- | | <a id="queryaimessagesrequestids"></a>`requestIds` | [`[ID!]`](#id) | Array of request IDs to fetch. | -| <a id="queryaimessagesroles"></a>`roles` | [`[AiChatMessageRole!]`](#aichatmessagerole) | Array of roles to fetch. | +| <a id="queryaimessagesroles"></a>`roles` | [`[AiMessageRole!]`](#aimessagerole) | Array of roles to fetch. | ### `Query.auditEventDefinitions` @@ -7951,28 +7951,28 @@ The edge type for [`AgentConfiguration`](#agentconfiguration). | <a id="agentconfigurationedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. | | <a id="agentconfigurationedgenode"></a>`node` | [`AgentConfiguration`](#agentconfiguration) | The item at the end of the edge. | -#### `AiChatMessageConnection` +#### `AiMessageConnection` -The connection type for [`AiChatMessage`](#aichatmessage). +The connection type for [`AiMessage`](#aimessage). ##### Fields | Name | Type | Description | | ---- | ---- | ----------- | -| <a id="aichatmessageconnectionedges"></a>`edges` | [`[AiChatMessageEdge]`](#aichatmessageedge) | A list of edges. | -| <a id="aichatmessageconnectionnodes"></a>`nodes` | [`[AiChatMessage]`](#aichatmessage) | A list of nodes. | -| <a id="aichatmessageconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. | +| <a id="aimessageconnectionedges"></a>`edges` | [`[AiMessageEdge]`](#aimessageedge) | A list of edges. | +| <a id="aimessageconnectionnodes"></a>`nodes` | [`[AiMessage]`](#aimessage) | A list of nodes. | +| <a id="aimessageconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. | -#### `AiChatMessageEdge` +#### `AiMessageEdge` -The edge type for [`AiChatMessage`](#aichatmessage). +The edge type for [`AiMessage`](#aimessage). ##### Fields | Name | Type | Description | | ---- | ---- | ----------- | -| <a id="aichatmessageedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. | -| <a id="aichatmessageedgenode"></a>`node` | [`AiChatMessage`](#aichatmessage) | The item at the end of the edge. | +| <a id="aimessageedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. | +| <a id="aimessageedgenode"></a>`node` | [`AiMessage`](#aimessage) | The item at the end of the edge. | #### `AlertManagementAlertConnection` @@ -13127,22 +13127,24 @@ Information about a connected Agent. | <a id="agentmetadatapodnamespace"></a>`podNamespace` | [`String`](#string) | Namespace of the pod running the Agent. | | <a id="agentmetadataversion"></a>`version` | [`String`](#string) | Agent version tag. | -### `AiChatMessage` +### `AiMessage` -GitLab Duo Chat message. +AI features communication message. #### Fields | Name | Type | Description | | ---- | ---- | ----------- | -| <a id="aichatmessagecontent"></a>`content` | [`String`](#string) | Content of the message. Can be null for failed responses. | -| <a id="aichatmessagecontenthtml"></a>`contentHtml` | [`String`](#string) | Content of the message in HTML format. Can be null for failed responses. | -| <a id="aichatmessageerrors"></a>`errors` | [`[String!]!`](#string) | Errors that occurred while asynchronously fetching an AI (assistant) response. | -| <a id="aichatmessageextras"></a>`extras` | [`AiMessageExtras`](#aimessageextras) | Extra message metadata. | -| <a id="aichatmessageid"></a>`id` | [`ID`](#id) | UUID of the message. | -| <a id="aichatmessagerequestid"></a>`requestId` | [`ID`](#id) | UUID of the original request message. Shared between chat prompt and response. | -| <a id="aichatmessagerole"></a>`role` | [`AiChatMessageRole!`](#aichatmessagerole) | Message role. | -| <a id="aichatmessagetimestamp"></a>`timestamp` | [`Time!`](#time) | Message timestamp. | +| <a id="aimessagechunkid"></a>`chunkId` | [`Int`](#int) | Incremental ID for a chunk from a streamed message. Null when it is not a streamed message. | +| <a id="aimessagecontent"></a>`content` | [`String`](#string) | Raw response content. | +| <a id="aimessagecontenthtml"></a>`contentHtml` | [`String`](#string) | Response content as HTML. | +| <a id="aimessageerrors"></a>`errors` | [`[String!]`](#string) | Message errors. | +| <a id="aimessageextras"></a>`extras` | [`AiMessageExtras`](#aimessageextras) | Extra message metadata. | +| <a id="aimessageid"></a>`id` | [`ID`](#id) | UUID of the message. | +| <a id="aimessagerequestid"></a>`requestId` | [`String`](#string) | UUID of the original request. Shared between chat prompt and response. | +| <a id="aimessagerole"></a>`role` | [`AiMessageRole!`](#aimessagerole) | Message owner role. | +| <a id="aimessagetimestamp"></a>`timestamp` | [`Time!`](#time) | Message creation timestamp. | +| <a id="aimessagetype"></a>`type` | [`AiMessageType`](#aimessagetype) | Message type. | ### `AiMessageExtras` @@ -13154,25 +13156,6 @@ Extra metadata for AI message. | ---- | ---- | ----------- | | <a id="aimessageextrassources"></a>`sources` | [`[JSON!]`](#json) | Sources used to form the message. | -### `AiResponse` - -#### Fields - -| Name | Type | Description | -| ---- | ---- | ----------- | -| <a id="airesponsechunkid"></a>`chunkId` | [`Int`](#int) | Incremental ID for a chunk from a streamed response. Null when it is not a streamed response. | -| <a id="airesponsecontent"></a>`content` | [`String`](#string) | Raw response content. | -| <a id="airesponsecontenthtml"></a>`contentHtml` | [`String`](#string) | Response content as HTML. | -| <a id="airesponseerrors"></a>`errors` | [`[String!]`](#string) | Errors return by AI API as response. | -| <a id="airesponseextras"></a>`extras` | [`AiMessageExtras`](#aimessageextras) | Extra message metadata. | -| <a id="airesponseid"></a>`id` | [`ID`](#id) | UUID of the message. | -| <a id="airesponserequestid"></a>`requestId` | [`String`](#string) | ID of the original request. | -| <a id="airesponseresponsebody"></a>`responseBody` **{warning-solid}** | [`String`](#string) | **Deprecated** in 16.4. Moved to content attribute. | -| <a id="airesponseresponsebodyhtml"></a>`responseBodyHtml` **{warning-solid}** | [`String`](#string) | **Deprecated** in 16.4. Moved to contentHtml attribute. | -| <a id="airesponserole"></a>`role` | [`AiChatMessageRole!`](#aichatmessagerole) | Message role. | -| <a id="airesponsetimestamp"></a>`timestamp` | [`Time!`](#time) | Message timestamp. | -| <a id="airesponsetype"></a>`type` | [`AiMessageType`](#aimessagetype) | Message type. | - ### `AlertManagementAlert` Describes an alert from the project's Alert Management. @@ -26758,15 +26741,15 @@ Agent token statuses. | <a id="agenttokenstatusactive"></a>`ACTIVE` | Active agent token. | | <a id="agenttokenstatusrevoked"></a>`REVOKED` | Revoked agent token. | -### `AiChatMessageRole` +### `AiMessageRole` -Roles to filter in chat message. +Possible message roles for AI features. | Value | Description | | ----- | ----------- | -| <a id="aichatmessageroleassistant"></a>`ASSISTANT` | Filter only assistant messages. | -| <a id="aichatmessagerolesystem"></a>`SYSTEM` | Filter only system messages. | -| <a id="aichatmessageroleuser"></a>`USER` | Filter only user messages. | +| <a id="aimessageroleassistant"></a>`ASSISTANT` | assistant message. | +| <a id="aimessagerolesystem"></a>`SYSTEM` | system message. | +| <a id="aimessageroleuser"></a>`USER` | user message. | ### `AiMessageType` diff --git a/doc/ci/migration/jenkins.md b/doc/ci/migration/jenkins.md index 5d6f3359daa..0af1cc84878 100644 --- a/doc/ci/migration/jenkins.md +++ b/doc/ci/migration/jenkins.md @@ -119,13 +119,13 @@ shared configuration in a hidden job: test-job: extends: - - .docker-config + - .test-config script: - bundle exec rake rspec lint-job: extends: - - .docker-config + - .test-config script: - yarn run prettier ``` diff --git a/doc/ci/pipelines/downstream_pipelines.md b/doc/ci/pipelines/downstream_pipelines.md index 6dc58882f37..21ed66ef939 100644 --- a/doc/ci/pipelines/downstream_pipelines.md +++ b/doc/ci/pipelines/downstream_pipelines.md @@ -68,9 +68,6 @@ Multi-project pipelines: - Are visible in the downstream project's pipeline list. - Are independent, so there are no nesting limits. -For more information, see the **Cross-project Pipeline Triggering and Visualization** demo at -[GitLab@learn](https://about.gitlab.com/learn/) in the **Continuous Integration** section. - If you use a public project to trigger downstream pipelines in a private project, make sure there are no confidentiality problems. The upstream project's pipelines page always displays: diff --git a/doc/ci/yaml/index.md b/doc/ci/yaml/index.md index ddb40d9f435..403152b32a9 100644 --- a/doc/ci/yaml/index.md +++ b/doc/ci/yaml/index.md @@ -7,13 +7,14 @@ type: reference # `.gitlab-ci.yml` keyword reference **(FREE ALL)** -This document lists the configuration options for your GitLab `.gitlab-ci.yml` file. +This document lists the configuration options for the GitLab `.gitlab-ci.yml` file. +This file is where you define the CI/CD jobs that make up your pipeline. +- To create your own `.gitlab-ci.yml` file, try a tutorial that demonstrates a + [simple](../quick_start/index.md) or [complex](../quick_start/tutorial.md) pipeline. - For a collection of examples, see [GitLab CI/CD examples](../examples/index.md). - To view a large `.gitlab-ci.yml` file used in an enterprise, see the [`.gitlab-ci.yml` file for `gitlab`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab-ci.yml). -- To create your own `.gitlab-ci.yml` file, try a tutorial that demonstrates a - [simple](../quick_start/index.md) or [complex](../quick_start/tutorial.md) pipeline. When you are editing your `.gitlab-ci.yml` file, you can validate it with the [CI Lint](../lint.md) tool. @@ -2768,8 +2769,8 @@ The `linux:rspec` job runs as soon as the `linux:build: [aws, app1]` job finishe ### `only` / `except` NOTE: -`only` and `except` are not being actively developed. [`rules`](#rules) is the preferred -keyword to control when to add jobs to pipelines. +`only` and `except` are not being actively developed. To control when to add jobs to pipelines, +use [`rules`](#rules) instead. You can use `only` and `except` to control when to add jobs to pipelines. @@ -2781,13 +2782,13 @@ for more details and examples. #### `only:refs` / `except:refs` +NOTE: +`only:refs` and `except:refs` are not being actively developed. To use refs, regular expressions, +or variables to control when to add jobs to pipelines, use [`rules:if`](#rulesif) instead. + Use the `only:refs` and `except:refs` keywords to control when to add jobs to a pipeline based on branch names or pipeline types. -`only:refs` and `except:refs` are not being actively developed. [`rules:if`](#rulesif) -is the preferred keyword when using refs, regular expressions, or variables to control -when to add jobs to pipelines. - **Keyword type**: Job keyword. You can use it only as part of a job. **Possible inputs**: An array including any number of: @@ -2870,13 +2871,13 @@ job2: #### `only:variables` / `except:variables` +NOTE: +`only:variables` and `except:variables` are not being actively developed. To use refs, +regular expressions, or variables to control when to add jobs to pipelines, use [`rules:if`](#rulesif) instead. + Use the `only:variables` or `except:variables` keywords to control when to add jobs to a pipeline, based on the status of [CI/CD variables](../variables/index.md). -`only:variables` and `except:variables` are not being actively developed. [`rules:if`](#rulesif) -is the preferred keyword when using refs, regular expressions, or variables to control -when to add jobs to pipelines. - **Keyword type**: Job keyword. You can use it only as part of a job. **Possible inputs**: @@ -2900,6 +2901,10 @@ deploy: #### `only:changes` / `except:changes` +NOTE: +`only:changes` and `except:changes` are not being actively developed. To use changed files +to control when to add a job to a pipeline, use [`rules:changes`](#ruleschanges) instead. + Use the `changes` keyword with `only` to run a job, or with `except` to skip a job, when a Git push event modifies a file. @@ -2909,9 +2914,6 @@ Use `changes` in pipelines with the following refs: - `external_pull_requests` - `merge_requests` (see additional details about [using `only:changes` with merge request pipelines](../jobs/job_control.md#use-onlychanges-with-merge-request-pipelines)) -`only:changes` and `except:changes` are not being actively developed. [`rules:changes`](#ruleschanges) -is the preferred keyword when using changed files to control when to add jobs to pipelines. - **Keyword type**: Job keyword. You can use it only as part of a job. **Possible inputs**: An array including any number of: @@ -2959,13 +2961,14 @@ docker build: #### `only:kubernetes` / `except:kubernetes` +NOTE: +`only:refs` and `except:refs` are not being actively developed. To control if jobs are added +to the pipeline when the Kubernetes service is active in the project, use [`rules:if`](#rulesif) +with the [`CI_KUBERNETES_ACTIVE`](../variables/predefined_variables.md) predefined CI/CD variable instead. + Use `only:kubernetes` or `except:kubernetes` to control if jobs are added to the pipeline when the Kubernetes service is active in the project. -`only:refs` and `except:refs` are not being actively developed. Use [`rules:if`](#rulesif) -with the [`CI_KUBERNETES_ACTIVE`](../variables/predefined_variables.md) predefined CI/CD variable -to control if jobs are added to the pipeline when the Kubernetes service is active in the project. - **Keyword type**: Job-specific. You can use it only as part of a job. **Possible inputs**: diff --git a/doc/development/bulk_import.md b/doc/development/bulk_import.md index 304aab9e3b3..081af2b4e17 100644 --- a/doc/development/bulk_import.md +++ b/doc/development/bulk_import.md @@ -6,7 +6,11 @@ info: To determine the technical writer assigned to the Stage/Group associated w # Group migration by direct transfer -[Introduced](https://gitlab.com/groups/gitlab-org/-/epics/2771) in GitLab 13.7. +> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/2771) in GitLab 13.7. + +NOTE: +To use direct transfer, ensure your GitLab installation is accessible from +[GitLab IP addresses](../user/gitlab_com/index.md#ip-range) and has a public DNS entry. [Group migration by direct transfer](../user/group/import/index.md#migrate-groups-by-direct-transfer-recommended) is the evolution of migrating groups and projects using file exports. The goal is to have an easier way for the user to migrate a whole group, diff --git a/doc/update/versions/gitlab_15_changes.md b/doc/update/versions/gitlab_15_changes.md index 12cbf5c5ec1..997397904dd 100644 --- a/doc/update/versions/gitlab_15_changes.md +++ b/doc/update/versions/gitlab_15_changes.md @@ -144,6 +144,9 @@ if you can't upgrade to 15.11.12 and later. - This issue may not manifest immediately as it can take up to a week before the Sidekiq is saturated enough. - Elasticsearch does not need to be enabled for this to occur. - To resolve this issue, upgrade to 15.11 or use the workaround in the issue. +- A bug with the [`BackfillTraversalIdsToBlobsAndWikiBlobs` advanced search migration](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/107730) might cause the Elasticsearch cluster to become saturated. + - When this issue occurs, searches might become slow and updates to the Elasticsearch cluster might take a long time to complete. + - To resolve this issue, upgrade to GitLab 15.10 to [reduce the migration batch size](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/113719). - **Upgrade to patch release 15.9.3 or later**. This provides fixes for two database migration bugs: - Patch releases 15.9.0, 15.9.1, 15.9.2 have a bug that can cause data loss from the user profile fields `linkedin`, `twitter`, `skype`, `website_url`, diff --git a/lib/gitlab/database/load_balancing/service_discovery.rb b/lib/gitlab/database/load_balancing/service_discovery.rb index 55926e486a4..3d11f0f88c1 100644 --- a/lib/gitlab/database/load_balancing/service_discovery.rb +++ b/lib/gitlab/database/load_balancing/service_discovery.rb @@ -151,13 +151,7 @@ module Gitlab # started just before we added the new hosts it will use an old # host/connection. While this connection will be checked in and out, # it won't be explicitly disconnected. - if Gitlab::Utils.to_boolean(ENV['LOAD_BALANCER_PARALLEL_DISCONNECT'], default: false) - disconnect_old_hosts(old_hosts) - else - old_hosts.each do |host| - host.disconnect!(timeout: disconnect_timeout) - end - end + disconnect_old_hosts(old_hosts) end # Returns an Array containing: diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 643b05d4003..d4277a26485 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -47626,6 +47626,9 @@ msgstr "" msgid "The selected image is too large." msgstr "" +msgid "The selected project is not available" +msgstr "" + msgid "The snippet can be accessed without any authentication." msgstr "" diff --git a/spec/frontend/contributors/component/contributors_spec.js b/spec/frontend/contributors/component/contributors_spec.js index f915b834aff..7d863a8eb78 100644 --- a/spec/frontend/contributors/component/contributors_spec.js +++ b/spec/frontend/contributors/component/contributors_spec.js @@ -8,6 +8,7 @@ import { HTTP_STATUS_OK } from '~/lib/utils/http_status'; import { visitUrl } from '~/lib/utils/url_utility'; import RefSelector from '~/ref/components/ref_selector.vue'; import { REF_TYPE_BRANCHES, REF_TYPE_TAGS } from '~/ref/constants'; +import { SET_CHART_DATA, SET_LOADING_STATE } from '~/contributors/stores/mutation_types'; jest.mock('~/lib/utils/url_utility', () => ({ visitUrl: jest.fn(), @@ -66,14 +67,14 @@ describe('Contributors charts', () => { }); it('should display loader whiled loading data', async () => { - wrapper.vm.$store.state.loading = true; + store.commit(SET_LOADING_STATE, true); await nextTick(); expect(findLoadingIcon().exists()).toBe(true); }); it('should render charts and a RefSelector when loading completed and there is chart data', async () => { - wrapper.vm.$store.state.loading = false; - wrapper.vm.$store.state.chartData = chartData; + store.commit(SET_LOADING_STATE, false); + store.commit(SET_CHART_DATA, chartData); await nextTick(); expect(findLoadingIcon().exists()).toBe(false); @@ -92,8 +93,8 @@ describe('Contributors charts', () => { }); it('should have a history button with a set href attribute', async () => { - wrapper.vm.$store.state.loading = false; - wrapper.vm.$store.state.chartData = chartData; + store.commit(SET_LOADING_STATE, false); + store.commit(SET_CHART_DATA, chartData); await nextTick(); const historyButton = findHistoryButton(); @@ -102,8 +103,8 @@ describe('Contributors charts', () => { }); it('visits a URL when clicking on a branch/tag', async () => { - wrapper.vm.$store.state.loading = false; - wrapper.vm.$store.state.chartData = chartData; + store.commit(SET_LOADING_STATE, false); + store.commit(SET_CHART_DATA, chartData); await nextTick(); findRefSelector().vm.$emit('input', branch); diff --git a/spec/frontend/integrations/gitlab_slack_application/components/projects_dropdown_spec.js b/spec/frontend/integrations/gitlab_slack_application/components/projects_dropdown_spec.js new file mode 100644 index 00000000000..8879a86a578 --- /dev/null +++ b/spec/frontend/integrations/gitlab_slack_application/components/projects_dropdown_spec.js @@ -0,0 +1,54 @@ +import { GlCollapsibleListbox } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import ProjectsDropdown from '~/integrations/gitlab_slack_application/components/projects_dropdown.vue'; + +describe('Slack application projects dropdown', () => { + let wrapper; + + const projectsMockData = [ + { + avatar_url: null, + id: 1, + name: 'Gitlab Smoke Tests', + name_with_namespace: 'Toolbox / Gitlab Smoke Tests', + }, + { + avatar_url: null, + id: 2, + name: 'Gitlab Test', + name_with_namespace: 'Gitlab Org / Gitlab Test', + }, + { + avatar_url: 'foo/bar', + id: 3, + name: 'Gitlab Shell', + name_with_namespace: 'Gitlab Org / Gitlab Shell', + }, + ]; + + const createComponent = (props = {}) => { + wrapper = shallowMount(ProjectsDropdown, { + propsData: { + projects: projectsMockData, + ...props, + }, + }); + }; + + const findListbox = () => wrapper.findComponent(GlCollapsibleListbox); + + beforeEach(() => { + createComponent(); + }); + + it('renders the listbox with 3 items', () => { + expect(findListbox().exists()).toBe(true); + expect(findListbox().props('items')).toHaveLength(3); + }); + + it('should emit project-selected if a project is clicked', () => { + findListbox().vm.$emit('select', 1); + + expect(wrapper.emitted('project-selected')).toMatchObject([[projectsMockData[0]]]); + }); +}); diff --git a/spec/lib/gitlab/database/load_balancing/service_discovery_spec.rb b/spec/lib/gitlab/database/load_balancing/service_discovery_spec.rb index 7197b99fe33..442fa678d4e 100644 --- a/spec/lib/gitlab/database/load_balancing/service_discovery_spec.rb +++ b/spec/lib/gitlab/database/load_balancing/service_discovery_spec.rb @@ -194,7 +194,6 @@ RSpec.describe Gitlab::Database::LoadBalancing::ServiceDiscovery, feature_catego describe '#replace_hosts' do before do - stub_env('LOAD_BALANCER_PARALLEL_DISCONNECT', 'true') allow(service) .to receive(:load_balancer) .and_return(load_balancer) @@ -257,26 +256,6 @@ RSpec.describe Gitlab::Database::LoadBalancing::ServiceDiscovery, feature_catego service.replace_hosts([address_foo, address_bar]) end end - - context 'when LOAD_BALANCER_PARALLEL_DISCONNECT is false' do - before do - stub_env('LOAD_BALANCER_PARALLEL_DISCONNECT', 'false') - end - - it 'disconnects them sequentially' do - host = load_balancer.host_list.hosts.first - - allow(service) - .to receive(:disconnect_timeout) - .and_return(2) - - expect(host) - .to receive(:disconnect!) - .with(timeout: 2) - - service.replace_hosts([address_bar]) - end - end end describe '#addresses_from_dns' do diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index b36737fc19d..f659f8552aa 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -5956,4 +5956,57 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev it { is_expected.to eq(expected) } end end + + describe '#current_patch_id_sha' do + let(:merge_request) { build_stubbed(:merge_request) } + let(:merge_request_diff) { build_stubbed(:merge_request_diff) } + let(:patch_id) { 'ghi789' } + + subject(:current_patch_id_sha) { merge_request.current_patch_id_sha } + + before do + allow(merge_request).to receive(:merge_request_diff).and_return(merge_request_diff) + allow(merge_request_diff).to receive(:patch_id_sha).and_return(patch_id) + end + + it { is_expected.to eq(patch_id) } + + context 'when related merge_request_diff does not have a patch_id_sha' do + let(:diff_refs) { instance_double(Gitlab::Diff::DiffRefs, base_sha: base_sha, head_sha: head_sha) } + let(:base_sha) { 'abc123' } + let(:head_sha) { 'def456' } + + before do + allow(merge_request_diff).to receive(:patch_id_sha).and_return(nil) + allow(merge_request).to receive(:diff_refs).and_return(diff_refs) + + allow_next_instance_of(Repository) do |repo| + allow(repo) + .to receive(:get_patch_id) + .with(diff_refs.base_sha, diff_refs.head_sha) + .and_return(patch_id) + end + end + + it { is_expected.to eq(patch_id) } + + context 'when base_sha is nil' do + let(:base_sha) { nil } + + it { is_expected.to be_nil } + end + + context 'when head_sha is nil' do + let(:head_sha) { nil } + + it { is_expected.to be_nil } + end + + context 'when base_sha and head_sha match' do + let(:head_sha) { base_sha } + + it { is_expected.to be_nil } + end + end + end end diff --git a/spec/services/merge_requests/approval_service_spec.rb b/spec/services/merge_requests/approval_service_spec.rb index 81fc5661032..e7fe5c19fa3 100644 --- a/spec/services/merge_requests/approval_service_spec.rb +++ b/spec/services/merge_requests/approval_service_spec.rb @@ -82,39 +82,12 @@ RSpec.describe MergeRequests::ApprovalService, feature_category: :code_review_wo it 'records a value' do service.execute(merge_request) - expect(merge_request.approvals.last.patch_id_sha).not_to be_nil + expect(merge_request.approvals.last.patch_id_sha).to eq(merge_request.current_patch_id_sha) end - context 'when base_sha is nil' do + context 'when MergeRequest#current_patch_id_sha is nil' do it 'records patch_id_sha as nil' do - expect_next_instance_of(Gitlab::Diff::DiffRefs) do |diff_ref| - expect(diff_ref).to receive(:base_sha).at_least(:once).and_return(nil) - end - - service.execute(merge_request) - - expect(merge_request.approvals.last.patch_id_sha).to be_nil - end - end - - context 'when head_sha is nil' do - it 'records patch_id_sha as nil' do - expect_next_instance_of(Gitlab::Diff::DiffRefs) do |diff_ref| - expect(diff_ref).to receive(:head_sha).at_least(:once).and_return(nil) - end - - service.execute(merge_request) - - expect(merge_request.approvals.last.patch_id_sha).to be_nil - end - end - - context 'when base_sha and head_sha match' do - it 'records patch_id_sha as nil' do - expect_next_instance_of(Gitlab::Diff::DiffRefs) do |diff_ref| - expect(diff_ref).to receive(:base_sha).at_least(:once).and_return("abc123") - expect(diff_ref).to receive(:head_sha).at_least(:once).and_return("abc123") - end + expect(merge_request).to receive(:current_patch_id_sha).and_return(nil) service.execute(merge_request) diff --git a/storybook/config/addons/gitlab_api_access/manager.js b/storybook/config/addons/gitlab_api_access/manager.js index 77d97c76ee2..55e7bcb9c4c 100644 --- a/storybook/config/addons/gitlab_api_access/manager.js +++ b/storybook/config/addons/gitlab_api_access/manager.js @@ -41,29 +41,35 @@ const GitLabAPIParametersPanel = () => { channel.emit(GITLAB_API_ACCESS_UPDATE_EVENT, state); - return h('div', {}, [ - h(Form.Field, { label: 'GitLab URL' }, [ + return h( + 'div', + {}, + h( + Form.Field, + { label: 'GitLab URL' }, h(Form.Input, { type: 'text', value: state.gitlabURL, placeholder: 'https://gitlab.com', onChange: (e) => updateGitLabURL(e), }), - ]), - h(Form.Field, { label: 'GitLab access token' }, [ + ), + h( + Form.Field, + { label: 'GitLab access token' }, h(Form.Input, { type: 'password', value: state.accessToken, onChange: (e) => updateAccessToken(e), }), - ]), - ]); + ), + ); }; addons.register(ADDON_ID, () => { addons.add(PANEL_ID, { type: types.PANEL, title: 'GitLab API Access', - render: ({ active, key }) => h(AddonPanel, { active, key }, [h(GitLabAPIParametersPanel)]), + render: ({ active, key }) => h(AddonPanel, { active, key }, h(GitLabAPIParametersPanel)), }); }); diff --git a/storybook/config/webpack.config.js b/storybook/config/webpack.config.js index d447211cbd8..cd2e4889300 100644 --- a/storybook/config/webpack.config.js +++ b/storybook/config/webpack.config.js @@ -1,10 +1,11 @@ /* eslint-disable no-param-reassign */ -const { statSync } = require('fs'); +const { statSync, existsSync } = require('fs'); const path = require('path'); const glob = require('glob'); const sass = require('sass'); const webpack = require('webpack'); +const { red } = require('chalk'); const IS_EE = require('../../config/helpers/is_ee_env'); const IS_JH = require('../../config/helpers/is_jh_env'); const gitlabWebpackConfig = require('../../config/webpack.config'); @@ -183,5 +184,21 @@ module.exports = function storybookWebpackConfig({ config }) { // By deleting the alias, imports of this path will resolve as expected. delete config.resolve.alias['@gitlab/svgs/dist/icons.svg']; + // Fail soft if a story requires a fixture, and the fixture file is absent. + // Without this, webpack fails at build phase, with a hard to read error. + // This rewrite rule pushes the error to be runtime. + config.plugins.push( + new webpack.NormalModuleReplacementPlugin(/^test_fixtures/, (resource) => { + const filename = resource.request.replace( + /^test_fixtures/, + config.resolve.alias.test_fixtures, + ); + if (!existsSync(filename)) { + console.error(red(`\nFixture '${filename}' wasn't found.\n`)); + resource.request = path.join(ROOT, 'storybook', 'fixture_stub.js'); + } + }), + ); + return config; }; diff --git a/storybook/fixture_stub.js b/storybook/fixture_stub.js new file mode 100644 index 00000000000..cd23e038e35 --- /dev/null +++ b/storybook/fixture_stub.js @@ -0,0 +1,12 @@ +/** + * This file is used as a substitute for absent fixture files. See fixture docs + * for how to generate or download them: + * https://docs.gitlab.com/ee/development/testing_guide/frontend_testing.html#frontend-test-fixtures + */ + +console.error( + // eslint-disable-next-line no-restricted-syntax + 'Some fixture files could not be found. Generate fixtures or download them. See docs for more: https://docs.gitlab.com/ee/development/testing_guide/frontend_testing.html#frontend-test-fixtures', +); + +export default null; |