diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_versions_list.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_versions_list.vue
new file mode 100644
index 00000000000..efc60c9c037
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_versions_list.vue
@@ -0,0 +1,57 @@
+
+
+
+
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql
index 8e50c95b10b..51e0ab5aba8 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql
+++ b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql
@@ -1,4 +1,10 @@
-query getPackageDetails($id: PackagesPackageID!) {
+query getPackageDetails(
+ $id: PackagesPackageID!
+ $first: Int
+ $last: Int
+ $after: String
+ $before: String
+) {
package(id: $id) {
id
name
@@ -55,7 +61,7 @@ query getPackageDetails($id: PackagesPackageID!) {
downloadPath
}
}
- versions(first: 100) {
+ versions(after: $after, before: $before, first: $first, last: $last) {
nodes {
id
name
@@ -69,6 +75,12 @@ query getPackageDetails($id: PackagesPackageID!) {
}
}
}
+ pageInfo {
+ hasNextPage
+ hasPreviousPage
+ endCursor
+ startCursor
+ }
}
dependencyLinks {
nodes {
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue b/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue
index eeed56b77c3..c59dcaee411 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue
@@ -22,7 +22,7 @@ import InstallationCommands from '~/packages_and_registries/package_registry/com
import PackageFiles from '~/packages_and_registries/package_registry/components/details/package_files.vue';
import PackageHistory from '~/packages_and_registries/package_registry/components/details/package_history.vue';
import PackageTitle from '~/packages_and_registries/package_registry/components/details/package_title.vue';
-import VersionRow from '~/packages_and_registries/package_registry/components/details/version_row.vue';
+import PackageVersionsList from '~/packages_and_registries/package_registry/components/details/package_versions_list.vue';
import DeletePackage from '~/packages_and_registries/package_registry/components/functional/delete_package.vue';
import {
PACKAGE_TYPE_NUGET,
@@ -48,6 +48,7 @@ import {
DELETE_MODAL_CONTENT,
DELETE_ALL_PACKAGE_FILES_MODAL_CONTENT,
DELETE_LAST_PACKAGE_FILE_MODAL_CONTENT,
+ GRAPHQL_PAGE_SIZE,
} from '~/packages_and_registries/package_registry/constants';
import destroyPackageFilesMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_package_files.mutation.graphql';
@@ -65,13 +66,13 @@ export default {
GlTabs,
GlSprintf,
PackageTitle,
- VersionRow,
DependencyRow,
PackageHistory,
AdditionalMetadata,
InstallationCommands,
PackageFiles,
DeletePackage,
+ PackageVersionsList,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -132,6 +133,7 @@ export default {
queryVariables() {
return {
id: convertToGraphQLId('Packages::Package', this.packageId),
+ first: GRAPHQL_PAGE_SIZE,
};
},
packageFiles() {
@@ -157,6 +159,9 @@ export default {
hasVersions() {
return this.packageEntity.versions?.nodes?.length > 0;
},
+ versionPageInfo() {
+ return this.packageEntity?.versions?.pageInfo ?? {};
+ },
packageDependencies() {
return this.packageEntity.dependencyLinks?.nodes || [];
},
@@ -264,6 +269,34 @@ export default {
resetDeleteModalContent() {
this.deletePackageModalContent = DELETE_MODAL_CONTENT;
},
+ updateQuery(_, { fetchMoreResult }) {
+ return fetchMoreResult;
+ },
+ fetchPreviousVersionsPage() {
+ const variables = {
+ ...this.queryVariables,
+ first: null,
+ last: GRAPHQL_PAGE_SIZE,
+ before: this.versionPageInfo?.startCursor,
+ };
+ this.$apollo.queries.packageEntity.fetchMore({
+ variables,
+ updateQuery: this.updateQuery,
+ });
+ },
+ fetchNextVersionsPage() {
+ const variables = {
+ ...this.queryVariables,
+ first: GRAPHQL_PAGE_SIZE,
+ last: null,
+ after: this.versionPageInfo?.endCursor,
+ };
+
+ this.$apollo.queries.packageEntity.fetchMore({
+ variables,
+ updateQuery: this.updateQuery,
+ });
+ },
},
i18n: {
DELETE_MODAL_TITLE,
@@ -271,6 +304,7 @@ export default {
deleteFileModalContent: s__(
`PackageRegistry|You are about to delete %{filename}. This is a destructive action that may render your package unusable. Are you sure?`,
),
+ otherVersionsTabTitle: __('Other versions'),
},
modal: {
packageDeletePrimaryAction: {
@@ -303,7 +337,7 @@ export default {
:description="s__('PackageRegistry|There was a problem fetching the details for this package.')"
:svg-path="emptyListIllustration"
/>
-
+
-
-
-
-
-
-
- {{ s__('PackageRegistry|There are no other versions of this package.') }}
-
+
+
+
+
+ {{ s__('PackageRegistry|There are no other versions of this package.') }}
+
+
+
diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb
index a8d3e980703..e05adc5cd0e 100644
--- a/app/helpers/diff_helper.rb
+++ b/app/helpers/diff_helper.rb
@@ -24,7 +24,7 @@ module DiffHelper
end
def show_only_context_commits?
- !!params[:only_context_commits] || @merge_request&.commits&.empty?
+ !!params[:only_context_commits] || @merge_request.has_no_commits?
end
def diff_options
diff --git a/config/open_api.yml b/config/open_api.yml
index f043932c71a..5502577e201 100644
--- a/config/open_api.yml
+++ b/config/open_api.yml
@@ -43,6 +43,8 @@ metadata:
description: Operations related to merge requests
- name: metadata
description: Operations related to metadata of the GitLab instance
+ - name: project_export
+ description: Operations related to export projects
- name: project_hooks
description: Operations related to project hooks
- name: project_import_bitbucket
@@ -55,5 +57,7 @@ metadata:
description: Operations related to releases
- name: suggestions
description: Operations related to suggestions
+ - name: system_hooks
+ description: Operations related to system hooks
- name: unleash_api
- description: Operations related to Unleash API
+ description: Operations related to Unleash API
\ No newline at end of file
diff --git a/db/post_migrate/20221104074652_add_temp_index_for_project_statistics_upload_size_migration.rb b/db/post_migrate/20221104074652_add_temp_index_for_project_statistics_upload_size_migration.rb
new file mode 100644
index 00000000000..b6ee636fa9b
--- /dev/null
+++ b/db/post_migrate/20221104074652_add_temp_index_for_project_statistics_upload_size_migration.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class AddTempIndexForProjectStatisticsUploadSizeMigration < Gitlab::Database::Migration[2.0]
+ INDEX_PROJECT_STATSISTICS_UPLOADS_SIZE = 'tmp_index_project_statistics_uploads_size'
+
+ disable_ddl_transaction!
+
+ def up
+ # Temporary index is to be used to trigger refresh for all project_statistics with
+ # upload_size <> 0
+ add_concurrent_index :project_statistics, [:project_id],
+ name: INDEX_PROJECT_STATSISTICS_UPLOADS_SIZE,
+ where: "uploads_size <> 0"
+ end
+
+ def down
+ remove_concurrent_index_by_name :project_statistics, INDEX_PROJECT_STATSISTICS_UPLOADS_SIZE
+ end
+end
diff --git a/db/schema_migrations/20221104074652 b/db/schema_migrations/20221104074652
new file mode 100644
index 00000000000..460f21a3f6e
--- /dev/null
+++ b/db/schema_migrations/20221104074652
@@ -0,0 +1 @@
+167032d562467c3d6be9e6c6c8c072f117e23798db35301f95386130ae115a00
\ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 2c69d2baea8..6cfd5aafc22 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -31217,6 +31217,8 @@ CREATE INDEX tmp_index_on_vulnerabilities_non_dismissed ON vulnerabilities USING
CREATE INDEX tmp_index_project_statistics_cont_registry_size ON project_statistics USING btree (project_id) WHERE (container_registry_size = 0);
+CREATE INDEX tmp_index_project_statistics_uploads_size ON project_statistics USING btree (project_id) WHERE (uploads_size <> 0);
+
CREATE INDEX tmp_index_vulnerability_occurrences_on_id_and_scanner_id ON vulnerability_occurrences USING btree (id, scanner_id) WHERE (report_type = ANY (ARRAY[7, 99]));
CREATE UNIQUE INDEX uniq_pkgs_deb_grp_architectures_on_distribution_id_and_name ON packages_debian_group_architectures USING btree (distribution_id, name);
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 7a7fc7b0403..6ef1666147c 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -6980,6 +6980,29 @@ The edge type for [`ContainerRepositoryTag`](#containerrepositorytag).
| `cursor` | [`String!`](#string) | A cursor for use in pagination. |
| `node` | [`ContainerRepositoryTag`](#containerrepositorytag) | The item at the end of the edge. |
+#### `ContributionAnalyticsContributionConnection`
+
+The connection type for [`ContributionAnalyticsContribution`](#contributionanalyticscontribution).
+
+##### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| `edges` | [`[ContributionAnalyticsContributionEdge]`](#contributionanalyticscontributionedge) | A list of edges. |
+| `nodes` | [`[ContributionAnalyticsContribution]`](#contributionanalyticscontribution) | A list of nodes. |
+| `pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
+
+#### `ContributionAnalyticsContributionEdge`
+
+The edge type for [`ContributionAnalyticsContribution`](#contributionanalyticscontribution).
+
+##### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| `cursor` | [`String!`](#string) | A cursor for use in pagination. |
+| `node` | [`ContributionAnalyticsContribution`](#contributionanalyticscontribution) | The item at the end of the edge. |
+
#### `CoverageFuzzingCorpusConnection`
The connection type for [`CoverageFuzzingCorpus`](#coveragefuzzingcorpus).
@@ -11236,6 +11259,24 @@ A tag from a container repository.
| `shortRevision` | [`String`](#string) | Short revision of the tag. |
| `totalSize` | [`BigInt`](#bigint) | Size of the tag. |
+### `ContributionAnalyticsContribution`
+
+Represents the contributions of a user.
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| `issuesClosed` | [`Int`](#int) | Number of issues closed by the user. |
+| `issuesCreated` | [`Int`](#int) | Number of issues created by the user. |
+| `mergeRequestsApproved` | [`Int`](#int) | Number of merge requests approved by the user. |
+| `mergeRequestsClosed` | [`Int`](#int) | Number of merge requests closed by the user. |
+| `mergeRequestsCreated` | [`Int`](#int) | Number of merge requests created by the user. |
+| `mergeRequestsMerged` | [`Int`](#int) | Number of merge requests merged by the user. |
+| `repoPushed` | [`Int`](#int) | Number of repository pushes the user made. |
+| `totalEvents` | [`Int`](#int) | Total number of events contributed by the user. |
+| `user` | [`UserCore`](#usercore) | Contributor User object. |
+
### `CoverageFuzzingCorpus`
Corpus for a coverage fuzzing job.
@@ -13079,6 +13120,23 @@ four standard [pagination arguments](#connection-pagination-arguments):
| `name` | [`String`](#string) | Filter the container repositories by their name. |
| `sort` | [`ContainerRepositorySort`](#containerrepositorysort) | Sort container repositories by this criteria. |
+##### `Group.contributions`
+
+Provides the aggregated contributions by users within the group and its subgroups.
+
+Returns [`ContributionAnalyticsContributionConnection`](#contributionanalyticscontributionconnection).
+
+This field returns a [connection](#connections). It accepts the
+four standard [pagination arguments](#connection-pagination-arguments):
+`before: String`, `after: String`, `first: Int`, `last: Int`.
+
+###### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| `from` | [`ISO8601Date!`](#iso8601date) | Start date of the reporting time range. |
+| `to` | [`ISO8601Date!`](#iso8601date) | End date of the reporting time range. The end date must be within 31 days after the start date. |
+
##### `Group.descendantGroups`
List of descendant groups of this group.
diff --git a/doc/architecture/blueprints/work_items/index.md b/doc/architecture/blueprints/work_items/index.md
index ee9688eeb39..89934801633 100644
--- a/doc/architecture/blueprints/work_items/index.md
+++ b/doc/architecture/blueprints/work_items/index.md
@@ -58,14 +58,19 @@ All Work Item types share the same pool of predefined widgets and are customized
### Work Item widget types (updating)
-- assignees
-- description
-- hierarchy
-- iteration
-- labels
-- start and due date
-- verification status
-- weight
+| widget type | feature flag |
+|---|---|---|
+| assignees | |
+| description | |
+| hierarchy | |
+| [iteration](https://gitlab.com/gitlab-org/gitlab/-/issues/367456) | work_items_mvc_2 |
+| [milestone](https://gitlab.com/gitlab-org/gitlab/-/issues/367463) | work_items_mvc_2 |
+| labels | |
+| start and due date | |
+| status\* | |
+| weight | |
+
+\* status is not currently a widget, but a part of the root work item, similar to title
### Work Item view
@@ -75,6 +80,16 @@ The new frontend view that renders Work Items of any type using global Work Item
Task is a special Work Item type. Tasks can be added to issues as child items and can be displayed in the modal on the issue view.
+### Feature flags
+
+Since this is a large project with numerous moving parts, feature flags are being used to track promotions of available widgets. The table below shows the different feature flags that are being used, and the audience that they are available to.
+
+| feature flag name | audience |
+|---|---|
+| `work_items` | defaulted to on |
+| `work_items_mvc` | `gitlab-org`, `gitlab-com` |
+| `work_items_mvc_2` | `gitlab-org/plan-stage` |
+
## Motivation
Work Items main goal is to enhance the planning toolset to become the most popular collaboration tool for knowledge workers in any industry.
diff --git a/doc/ci/pipelines/downstream_pipelines.md b/doc/ci/pipelines/downstream_pipelines.md
index a711e938503..522466487a2 100644
--- a/doc/ci/pipelines/downstream_pipelines.md
+++ b/doc/ci/pipelines/downstream_pipelines.md
@@ -122,7 +122,7 @@ the value of the [`$CI_PIPELINE_SOURCE` predefined variable](../variables/predef
for all jobs is:
- `pipeline` for multi-project pipelines.
-- `parent` for parent-child pipelines.
+- `parent_pipeline` for parent-child pipelines.
For example, to control jobs in multi-project pipelines in a project that also runs
merge request pipelines:
diff --git a/doc/ci/services/index.md b/doc/ci/services/index.md
index 18932512a75..830b9406c6e 100644
--- a/doc/ci/services/index.md
+++ b/doc/ci/services/index.md
@@ -392,6 +392,46 @@ time.
1. Check the exit status of build script.
1. Remove the build container and all created service containers.
+## Capturing service container logs
+
+Logs generated by applications running in service containers can be captured for subsequent examination and debugging.
+You might want to look at service container's logs when the service container has started successfully, but is not
+behaving es expected, leading to job failures. The logs can indicate missing or incorrect configuration of the service
+within the container.
+
+`CI_DEBUG_SERVICES` should only by enabled when service containers are being actively debugged as there are both storage
+and performance consequences to capturing service container logs.
+
+To enable service logging, add the `CI_DEBUG_SERVICES` variable to the project's
+`.gitlab-ci.yml` file:
+
+```yaml
+variables:
+ CI_DEBUG_SERVICES: "true"
+```
+
+Accepted values are:
+
+- Enabled: `TRUE`, `true`, `True`
+- Disabled: `FALSE`, `false`, `False`
+
+Any other values will result in an error message and effectively disable the feature.
+
+When enabled, logs for _all_ service containers will be captured and streamed into the jobs trace log concurrently with
+other logs. Logs from each container will be prefixed with the container's aliases, and displayed in a different color.
+
+NOTE:
+You may want to adjust the logging level in the service container for which you want to capture logs since the default
+logging level may not provide sufficient details to diagnose job failures.
+
+WARNING:
+Enabling `CI_DEBUG_SERVICES` _may_ result in masked variables being revealed. When `CI_DEBUG_SERVICES` is enabled,
+service container logs and the CI job's logs are streamed to the job's trace log _concurrently_, which makes it possible
+for a service container log to be inserted _inside_ a job's masked log. This would thwart the variable masking mechanism
+and result in the masked variable being revealed.
+
+See [Mask a CI/CD Variable](../variables/index.md#mask-a-cicd-variable)
+
## Debug a job locally
The following commands are run without root privileges. You should be
diff --git a/doc/ci/variables/index.md b/doc/ci/variables/index.md
index 7ad42aaf96b..cd592310621 100644
--- a/doc/ci/variables/index.md
+++ b/doc/ci/variables/index.md
@@ -357,15 +357,22 @@ The value of the variable must:
- The `~` character (In [GitLab 13.12 and later](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/61517)).
- Not match the name of an existing predefined or custom CI/CD variable.
-NOTE:
-Masking a CI/CD variable is not a guaranteed way to prevent malicious users from accessing
-variable values. To make variables more secure, you can [use external secrets](../secrets/index.md).
-
WARNING:
-Due to a technical limitation, masked variables that are more than 4 KiB in length are not recommended. Printing such
-a large value to the trace log has the potential to be [revealed](https://gitlab.com/gitlab-org/gitlab-runner/-/issues/28128).
-When using GitLab Runner 14.2, only the tail of the variable, characters beyond 4KiB in length, have the potential to
-be revealed.
+Masking a CI/CD variable is not a guaranteed way to prevent malicious users from
+accessing variable values. The masking feature is "best-effort" and there to
+help when a variable is accidentally revealed. To make variables more secure,
+consider using [external secrets](../secrets/index.md) and [file type variables](#cicd-variable-types)
+to prevent commands such as `env`/`printenv` from printing secret variables.
+
+Runner versions implement masking in different ways, some with technical
+limitations. Below is a table of such limitations.
+
+| Version from | Version to | Limitations |
+| ------------ | ---------- | ------ |
+| v0.1.0 | v11.8.0 | No masking functionality supported. |
+| v11.9.0 | v14.1.0 | Masking of large (> 4KiB) secrets could potentially be [revealed](https://gitlab.com/gitlab-org/gitlab-runner/-/issues/28128). No sensitive URL parameter masking. |
+| v14.2.0 | v15.3.0 | The tail of a large (> 4KiB) secret could potentially be [revealed](https://gitlab.com/gitlab-org/gitlab-runner/-/issues/28128). No sensitive URL parameter masking. |
+| v15.7.0 | | Potential for secrets to be revealed when `CI_DEBUG_SERVICES` is enabled. For details, read about [service container logging](../services/index.md#capturing-service-container-logs). |
### Protected CI/CD variables
diff --git a/doc/ci/variables/predefined_variables.md b/doc/ci/variables/predefined_variables.md
index faa96d67809..852110a1415 100644
--- a/doc/ci/variables/predefined_variables.md
+++ b/doc/ci/variables/predefined_variables.md
@@ -46,6 +46,7 @@ as it can cause the pipeline to behave unexpectedly.
| `CI_CONCURRENT_PROJECT_ID` | all | 11.10 | The unique ID of build execution in a single executor and project. |
| `CI_CONFIG_PATH` | 9.4 | 0.5 | The path to the CI/CD configuration file. Defaults to `.gitlab-ci.yml`. Read-only inside a running pipeline. |
| `CI_DEBUG_TRACE` | all | 1.7 | `true` if [debug logging (tracing)](index.md#debug-logging) is enabled. |
+| `CI_DEBUG_SERVICES` | 15.7 | 15.7 | `true` if [service container logging](../services/index.md#capturing-service-container-logs) is enabled. |
| `CI_DEFAULT_BRANCH` | 12.4 | all | The name of the project's default branch. |
| `CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX` | 13.7 | all | The top-level group image prefix for pulling images through the Dependency Proxy. |
| `CI_DEPENDENCY_PROXY_DIRECT_GROUP_IMAGE_PREFIX` | 14.3 | all | The direct group image prefix for pulling images through the Dependency Proxy. |
diff --git a/doc/user/profile/notifications.md b/doc/user/profile/notifications.md
index fd5c037fe3a..1deb4842107 100644
--- a/doc/user/profile/notifications.md
+++ b/doc/user/profile/notifications.md
@@ -357,13 +357,13 @@ a merge request or an issue.
The following table lists all GitLab-specific email headers:
| Header | Description |
-|---------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------|
+| ------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `List-Id` | The path of the project in an RFC 2919 mailing list identifier. You can use it for email organization with filters. |
| `X-GitLab-(Resource)-ID` | The ID of the resource the notification is for. The resource, for example, can be `Issue`, `MergeRequest`, `Commit`, or another such resource. |
| `X-GitLab-Discussion-ID` | The ID of the thread the comment belongs to, in notification emails for comments. |
| `X-GitLab-Group-Id` | The group's ID. Only present on notification emails for [epics](../group/epics/index.md). |
| `X-GitLab-Group-Path` | The group's path. Only present on notification emails for [epics](../group/epics/index.md) |
-| [`X-GitLab-NotificationReason`](#x-gitlab-notificationreason) | The reason for the notification. This can be `mentioned`, `assigned`, or `own_activity`. |
+| `X-GitLab-NotificationReason` | The reason for the notification. [See possible values.](#x-gitlab-notificationreason). |
| `X-GitLab-Pipeline-Id` | The ID of the pipeline the notification is for, in notification emails for pipelines. |
| `X-GitLab-Project-Id` | The project's ID. |
| `X-GitLab-Project-Path` | The project's path. |
@@ -377,21 +377,35 @@ The value is one of the following, in order of priority:
- `own_activity`
- `assigned`
+- `review_requested`
- `mentioned`
+- `subscribed`
The reason for the notification is also included in the footer of the notification email.
For example, an email with the reason `assigned` has this sentence in the footer:
> You are receiving this email because you have been assigned an item on \.
-For example, an alert notification email can have one of
-[the alert's](../../operations/incident_management/alerts.md) statuses:
+#### On-call alerts notifications **(PREMIUM)**
+
+An [on-call alert](../../operations/incident_management/oncall_schedules.md)
+notification email can have one of [the alert's](../../operations/incident_management/alerts.md) statuses:
- `alert_triggered`
- `alert_acknowledged`
- `alert_resolved`
- `alert_ignored`
+#### Incident escalation notifications **(PREMIUM)**
+
+An [incident escalation](../../operations/incident_management/escalation_policies.md)
+notification email can have one of [the incident's](../../operations/incident_management/incidents.md) status:
+
+- `incident_triggered`
+- `incident_acknowledged`
+- `incident_resolved`
+- `incident_ignored`
+
Expanding the list of events included in the `X-GitLab-NotificationReason` header is tracked in
[issue 20689](https://gitlab.com/gitlab-org/gitlab/-/issues/20689).
diff --git a/lib/api/api.rb b/lib/api/api.rb
index c5794db5580..7b9e56c5271 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -193,6 +193,7 @@ module API
mount ::API::Metadata
mount ::API::MergeRequestDiffs
mount ::API::PersonalAccessTokens::SelfInformation
+ mount ::API::ProjectExport
mount ::API::ProjectHooks
mount ::API::ProjectRepositoryStorageMoves
mount ::API::Releases
@@ -205,6 +206,7 @@ module API
mount ::API::Statistics
mount ::API::Submodules
mount ::API::Suggestions
+ mount ::API::SystemHooks
mount ::API::Tags
mount ::API::Unleash
mount ::API::UserCounts
@@ -293,7 +295,6 @@ module API
mount ::API::ProjectContainerRepositories
mount ::API::ProjectDebianDistributions
mount ::API::ProjectEvents
- mount ::API::ProjectExport
mount ::API::ProjectImport
mount ::API::ProjectMilestones
mount ::API::ProjectPackages
@@ -315,7 +316,6 @@ module API
mount ::API::SidekiqMetrics
mount ::API::Snippets
mount ::API::Subscriptions
- mount ::API::SystemHooks
mount ::API::Tags
mount ::API::Templates
mount ::API::Terraform::Modules::V1::Packages
diff --git a/lib/api/ci/jobs.rb b/lib/api/ci/jobs.rb
index 770420f8f93..2fe270bcb8a 100644
--- a/lib/api/ci/jobs.rb
+++ b/lib/api/ci/jobs.rb
@@ -244,6 +244,8 @@ module API
# current_authenticated_job will be nil if user is using
# a valid authentication (like PRIVATE-TOKEN) that is not CI_JOB_TOKEN
not_found!('Job') unless current_authenticated_job
+
+ ::Gitlab::ApplicationContext.push(job: current_authenticated_job)
end
end
end
diff --git a/lib/api/entities/bulk_imports/export_status.rb b/lib/api/entities/bulk_imports/export_status.rb
index c9c7f34a16a..fee983c6fd8 100644
--- a/lib/api/entities/bulk_imports/export_status.rb
+++ b/lib/api/entities/bulk_imports/export_status.rb
@@ -4,10 +4,10 @@ module API
module Entities
module BulkImports
class ExportStatus < Grape::Entity
- expose :relation
- expose :status
- expose :error
- expose :updated_at
+ expose :relation, documentation: { type: 'string', example: 'issues' }
+ expose :status, documentation: { type: 'string', example: 'started', values: %w[started finished failed] }
+ expose :error, documentation: { type: 'string', example: 'Error message' }
+ expose :updated_at, documentation: { type: 'dateTime', example: '2012-05-28T04:42:42-07:00' }
end
end
end
diff --git a/lib/api/entities/hook.rb b/lib/api/entities/hook.rb
index 95924321221..e24e201ac57 100644
--- a/lib/api/entities/hook.rb
+++ b/lib/api/entities/hook.rb
@@ -3,12 +3,18 @@
module API
module Entities
class Hook < Grape::Entity
- expose :id, :url, :created_at, :push_events, :tag_push_events, :merge_requests_events, :repository_update_events
- expose :enable_ssl_verification
+ expose :id, documentation: { type: 'string', example: 1 }
+ expose :url, documentation: { type: 'string', example: 'https://webhook.site' }
+ expose :created_at, documentation: { type: 'dateTime', example: '2012-05-28T04:42:42-07:00' }
+ expose :push_events, documentation: { type: 'boolean' }
+ expose :tag_push_events, documentation: { type: 'boolean' }
+ expose :merge_requests_events, documentation: { type: 'boolean' }
+ expose :repository_update_events, documentation: { type: 'boolean' }
+ expose :enable_ssl_verification, documentation: { type: 'boolean' }
- expose :alert_status
- expose :disabled_until
- expose :url_variables
+ expose :alert_status, documentation: { type: 'symbol', example: :executable }
+ expose :disabled_until, documentation: { type: 'dateTime', example: '2012-05-28T04:42:42-07:00' }
+ expose :url_variables, documentation: { type: 'Hash', example: { "token" => "secr3t" }, is_array: true }
def url_variables
object.url_variables.keys.map { { key: _1 } }
diff --git a/lib/api/entities/project_export_status.rb b/lib/api/entities/project_export_status.rb
index ad84a45996a..9a2aeb7a6bb 100644
--- a/lib/api/entities/project_export_status.rb
+++ b/lib/api/entities/project_export_status.rb
@@ -5,13 +5,21 @@ module API
class ProjectExportStatus < ProjectIdentity
include ::API::Helpers::RelatedResourcesHelpers
- expose :export_status
+ expose :export_status, documentation: {
+ type: 'string', example: 'finished', values: %w[queued started finished failed]
+ }
expose :_links, if: lambda { |project, _options| project.export_status == :finished } do
- expose :api_url do |project|
+ expose :api_url, documentation: {
+ type: 'string',
+ example: 'https://gitlab.example.com/api/v4/projects/1/export/download'
+ } do |project|
expose_url(api_v4_projects_export_download_path(id: project.id))
end
- expose :web_url do |project|
+ expose :web_url, documentation: {
+ type: 'string',
+ example: 'https://gitlab.example.com/gitlab-org/gitlab-test/download_export'
+ } do |project|
Gitlab::Routing.url_helpers.download_export_project_url(project)
end
end
diff --git a/lib/api/project_export.rb b/lib/api/project_export.rb
index 8f3f50adc86..e4e950fb603 100644
--- a/lib/api/project_export.rb
+++ b/lib/api/project_export.rb
@@ -16,7 +16,14 @@ module API
resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get export status' do
detail 'This feature was introduced in GitLab 10.6.'
- success Entities::ProjectExportStatus
+ success code: 200, model: Entities::ProjectExportStatus
+ failure [
+ { code: 401, message: 'Unauthorized' },
+ { code: 403, message: 'Forbidden' },
+ { code: 404, message: 'Not found' },
+ { code: 503, message: 'Service unavailable' }
+ ]
+ tags ['project_export']
end
get ':id/export' do
present user_project, with: Entities::ProjectExportStatus
@@ -24,6 +31,15 @@ module API
desc 'Download export' do
detail 'This feature was introduced in GitLab 10.6.'
+ success code: 200
+ failure [
+ { code: 401, message: 'Unauthorized' },
+ { code: 403, message: 'Forbidden' },
+ { code: 404, message: 'Not found' },
+ { code: 503, message: 'Service unavailable' }
+ ]
+ tags ['project_export']
+ produces %w[application/octet-stream application/json]
end
get ':id/export/download' do
check_rate_limit! :project_download_export, scope: [current_user, user_project.namespace]
@@ -41,6 +57,16 @@ module API
desc 'Start export' do
detail 'This feature was introduced in GitLab 10.6.'
+ success code: 202
+ failure [
+ { code: 400, message: 'Bad request' },
+ { code: 401, message: 'Unauthorized' },
+ { code: 403, message: 'Forbidden' },
+ { code: 404, message: 'Not found' },
+ { code: 429, message: 'Too many requests' },
+ { code: 503, message: 'Service unavailable' }
+ ]
+ tags ['project_export']
end
params do
optional :description, type: String, desc: 'Override the project description'
@@ -86,6 +112,15 @@ module API
desc 'Start relations export' do
detail 'This feature was introduced in GitLab 14.4'
+ success code: 202
+ failure [
+ { code: 400, message: 'Bad request' },
+ { code: 401, message: 'Unauthorized' },
+ { code: 403, message: 'Forbidden' },
+ { code: 404, message: 'Not found' },
+ { code: 503, message: 'Service unavailable' }
+ ]
+ tags ['project_export']
end
post ':id/export_relations' do
response = ::BulkImports::ExportService.new(portable: user_project, user: current_user).execute
@@ -93,12 +128,23 @@ module API
if response.success?
accepted!
else
- render_api_error!(message: 'Project relations export could not be started.')
+ render_api_error!('Project relations export could not be started.', 500)
end
end
desc 'Download relations export' do
detail 'This feature was introduced in GitLab 14.4'
+ success code: 200
+ failure [
+ { code: 400, message: 'Bad request' },
+ { code: 401, message: 'Unauthorized' },
+ { code: 403, message: 'Forbidden' },
+ { code: 404, message: 'Not found' },
+ { code: 500, message: 'Internal Server Error' },
+ { code: 503, message: 'Service unavailable' }
+ ]
+ tags ['project_export']
+ produces %w[application/octet-stream application/json]
end
params do
requires :relation,
@@ -119,6 +165,15 @@ module API
desc 'Relations export status' do
detail 'This feature was introduced in GitLab 14.4'
+ is_array true
+ success code: 200, model: Entities::BulkImports::ExportStatus
+ failure [
+ { code: 401, message: 'Unauthorized' },
+ { code: 403, message: 'Forbidden' },
+ { code: 404, message: 'Not found' },
+ { code: 503, message: 'Service unavailable' }
+ ]
+ tags ['project_export']
end
get ':id/export_relations/status' do
present user_project.bulk_import_exports, with: Entities::BulkImports::ExportStatus
diff --git a/lib/api/system_hooks.rb b/lib/api/system_hooks.rb
index 804cedfefe9..f2019d785a0 100644
--- a/lib/api/system_hooks.rb
+++ b/lib/api/system_hooks.rb
@@ -4,6 +4,8 @@ module API
class SystemHooks < ::API::Base
include PaginationParams
+ system_hooks_tags = %w[system_hooks]
+
feature_category :integrations
before do
@@ -19,12 +21,13 @@ module API
end
params :hook_parameters do
- optional :token, type: String, desc: 'The token used to validate payloads'
- optional :push_events, type: Boolean, desc: "Trigger hook on push events"
- optional :tag_push_events, type: Boolean, desc: "Trigger hook on tag push events"
- optional :merge_requests_events, type: Boolean, desc: "Trigger hook on tag push events"
- optional :repository_update_events, type: Boolean, desc: "Trigger hook on repository update events"
- optional :enable_ssl_verification, type: Boolean, desc: "Do SSL verification when triggering the hook"
+ optional :token, type: String,
+ desc: "Secret token to validate received payloads; this isn't returned in the response"
+ optional :push_events, type: Boolean, desc: 'When true, the hook fires on push events'
+ optional :tag_push_events, type: Boolean, desc: 'When true, the hook fires on new tags being pushed'
+ optional :merge_requests_events, type: Boolean, desc: 'Trigger hook on merge requests events'
+ optional :repository_update_events, type: Boolean, desc: 'Trigger hook on repository update events'
+ optional :enable_ssl_verification, type: Boolean, desc: 'Do SSL verification when triggering the hook'
use :url_variables
end
end
@@ -32,8 +35,11 @@ module API
resource :hooks do
mount ::API::Hooks::UrlVariables
- desc 'Get the list of system hooks' do
+ desc 'List system hooks' do
+ detail 'Get a list of all system hooks'
success Entities::Hook
+ is_array true
+ tags system_hooks_tags
end
params do
use :pagination
@@ -42,8 +48,13 @@ module API
present paginate(SystemHook.all), with: Entities::Hook
end
- desc 'Get a hook' do
+ desc 'Get system hook' do
+ detail 'Get a system hook by its ID. Introduced in GitLab 14.9.'
success Entities::Hook
+ failure [
+ { code: 404, message: 'Not found' }
+ ]
+ tags system_hooks_tags
end
params do
requires :hook_id, type: Integer, desc: 'The ID of the system hook'
@@ -52,8 +63,15 @@ module API
present find_hook, with: Entities::Hook
end
- desc 'Create a new system hook' do
+ desc 'Add new system hook' do
+ detail 'Add a new system hook'
success Entities::Hook
+ failure [
+ { code: 400, message: 'Validation error' },
+ { code: 404, message: 'Not found' },
+ { code: 422, message: 'Unprocessable entity' }
+ ]
+ tags system_hooks_tags
end
params do
use :requires_url
@@ -66,11 +84,18 @@ module API
save_hook(hook, Entities::Hook)
end
- desc 'Update an existing system hook' do
+ desc 'Edit system hook' do
+ detail 'Edits a system hook'
success Entities::Hook
+ failure [
+ { code: 400, message: 'Validation error' },
+ { code: 404, message: 'Not found' },
+ { code: 422, message: 'Unprocessable entity' }
+ ]
+ tags system_hooks_tags
end
params do
- requires :hook_id, type: Integer, desc: "The ID of the hook to update"
+ requires :hook_id, type: Integer, desc: 'The ID of the system hook'
use :optional_url
use :hook_parameters
end
@@ -90,8 +115,13 @@ module API
kind: 'system_hooks'
}
- desc 'Delete a hook' do
+ desc 'Delete system hook' do
+ detail 'Deletes a system hook'
success Entities::Hook
+ failure [
+ { code: 404, message: 'Not found' }
+ ]
+ tags system_hooks_tags
end
params do
requires :hook_id, type: Integer, desc: 'The ID of the system hook'
diff --git a/lib/gitlab/ci/templates/Terraform.gitlab-ci.yml b/lib/gitlab/ci/templates/Terraform.gitlab-ci.yml
index 4d0259fe678..51bcbd278d5 100644
--- a/lib/gitlab/ci/templates/Terraform.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Terraform.gitlab-ci.yml
@@ -12,6 +12,7 @@ stages:
- test
- build
- deploy
+ - cleanup
fmt:
extends: .terraform:fmt
diff --git a/lib/gitlab/ci/templates/Terraform.latest.gitlab-ci.yml b/lib/gitlab/ci/templates/Terraform.latest.gitlab-ci.yml
index 019b970bc30..0b6c10293fc 100644
--- a/lib/gitlab/ci/templates/Terraform.latest.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Terraform.latest.gitlab-ci.yml
@@ -12,6 +12,7 @@ stages:
- test
- build
- deploy
+ - cleanup
fmt:
extends: .terraform:fmt
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index e1f184dbd36..005786d3e90 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -10782,6 +10782,12 @@ msgstr ""
msgid "ContributionAnalytics|No pushes for the selected time period."
msgstr ""
+msgid "ContributionAnalytics|The given date range is larger than 31 days"
+msgstr ""
+
+msgid "ContributionAnalytics|The to date is earlier than the given from date"
+msgstr ""
+
msgid "ContributionAnalytics|There is too much data to calculate. Try lowering the period_limit setting in the insights configuration file."
msgstr ""
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/package_versions_list_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/package_versions_list_spec.js
new file mode 100644
index 00000000000..f0fa9592419
--- /dev/null
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/package_versions_list_spec.js
@@ -0,0 +1,152 @@
+import { GlKeysetPagination } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import PackageVersionsList from '~/packages_and_registries/package_registry/components/details/package_versions_list.vue';
+import PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.vue';
+import VersionRow from '~/packages_and_registries/package_registry/components/details/version_row.vue';
+import { packageData } from '../../mock_data';
+
+describe('PackageVersionsList', () => {
+ let wrapper;
+
+ const EmptySlotStub = { name: 'empty-slot-stub', template: 'empty message
' };
+ const packageList = [
+ packageData({
+ name: 'version 1',
+ }),
+ packageData({
+ id: `gid://gitlab/Packages::Package/112`,
+ name: 'version 2',
+ }),
+ ];
+
+ const uiElements = {
+ findLoader: () => wrapper.findComponent(PackagesListLoader),
+ findListPagination: () => wrapper.findComponent(GlKeysetPagination),
+ findEmptySlot: () => wrapper.findComponent(EmptySlotStub),
+ findListRow: () => wrapper.findAllComponents(VersionRow),
+ };
+ const mountComponent = (props) => {
+ wrapper = shallowMountExtended(PackageVersionsList, {
+ propsData: {
+ versions: packageList,
+ pageInfo: {},
+ isLoading: false,
+ ...props,
+ },
+ slots: {
+ 'empty-state': EmptySlotStub,
+ },
+ });
+ };
+
+ describe('when list is loading', () => {
+ beforeEach(() => {
+ mountComponent({ isLoading: true, versions: [] });
+ });
+ it('displays loader', () => {
+ expect(uiElements.findLoader().exists()).toBe(true);
+ });
+
+ it('does not display rows', () => {
+ expect(uiElements.findListRow().exists()).toBe(false);
+ });
+
+ it('does not display empty slot message', () => {
+ expect(uiElements.findEmptySlot().exists()).toBe(false);
+ });
+
+ it('does not display pagination', () => {
+ expect(uiElements.findListPagination().exists()).toBe(false);
+ });
+ });
+
+ describe('when list is loaded and has no data', () => {
+ beforeEach(() => {
+ mountComponent({ isLoading: false, versions: [] });
+ });
+
+ it('displays empty slot message', () => {
+ expect(uiElements.findEmptySlot().exists()).toBe(true);
+ });
+
+ it('does not display loader', () => {
+ expect(uiElements.findLoader().exists()).toBe(false);
+ });
+
+ it('does not display rows', () => {
+ expect(uiElements.findListRow().exists()).toBe(false);
+ });
+
+ it('does not display pagination', () => {
+ expect(uiElements.findListPagination().exists()).toBe(false);
+ });
+ });
+
+ describe('when list is loaded with data', () => {
+ beforeEach(() => {
+ mountComponent();
+ });
+
+ it('displays package version rows', () => {
+ expect(uiElements.findListRow().exists()).toEqual(true);
+ expect(uiElements.findListRow()).toHaveLength(packageList.length);
+ });
+
+ it('binds the correct props', () => {
+ expect(uiElements.findListRow().at(0).props()).toMatchObject({
+ packageEntity: expect.objectContaining(packageList[0]),
+ });
+
+ expect(uiElements.findListRow().at(1).props()).toMatchObject({
+ packageEntity: expect.objectContaining(packageList[1]),
+ });
+ });
+
+ describe('pagination display', () => {
+ it('does not display pagination if there is no previous or next page', () => {
+ expect(uiElements.findListPagination().exists()).toBe(false);
+ });
+
+ it('displays pagination if pageInfo.hasNextPage is true', async () => {
+ await wrapper.setProps({ pageInfo: { hasNextPage: true } });
+ expect(uiElements.findListPagination().exists()).toBe(true);
+ });
+
+ it('displays pagination if pageInfo.hasPreviousPage is true', async () => {
+ await wrapper.setProps({ pageInfo: { hasPreviousPage: true } });
+ expect(uiElements.findListPagination().exists()).toBe(true);
+ });
+
+ it('displays pagination if both pageInfo.hasNextPage and pageInfo.hasPreviousPage are true', async () => {
+ await wrapper.setProps({ pageInfo: { hasNextPage: true, hasPreviousPage: true } });
+ expect(uiElements.findListPagination().exists()).toBe(true);
+ });
+ });
+
+ it('does not display loader', () => {
+ expect(uiElements.findLoader().exists()).toBe(false);
+ });
+
+ it('does not display empty slot message', () => {
+ expect(uiElements.findEmptySlot().exists()).toBe(false);
+ });
+ });
+
+ describe('when user interacts with pagination', () => {
+ beforeEach(() => {
+ mountComponent({ pageInfo: { hasNextPage: true } });
+ });
+
+ it('emits prev-page event when paginator emits prev event', () => {
+ uiElements.findListPagination().vm.$emit('prev');
+
+ expect(wrapper.emitted('prev-page')).toHaveLength(1);
+ });
+
+ it('emits next-page when paginator emits next event', () => {
+ uiElements.findListPagination().vm.$emit('next');
+
+ expect(wrapper.emitted('next-page')).toHaveLength(1);
+ });
+ });
+});
diff --git a/spec/frontend/packages_and_registries/package_registry/mock_data.js b/spec/frontend/packages_and_registries/package_registry/mock_data.js
index c2b6fb734d6..f247c83c85f 100644
--- a/spec/frontend/packages_and_registries/package_registry/mock_data.js
+++ b/spec/frontend/packages_and_registries/package_registry/mock_data.js
@@ -233,6 +233,12 @@ export const packageDetailsQuery = (extendPackage) => ({
},
versions: {
nodes: packageVersions(),
+ pageInfo: {
+ hasNextPage: true,
+ hasPreviousPage: false,
+ endCursor: 'endCursor',
+ startCursor: 'startCursor',
+ },
__typename: 'PackageConnection',
},
dependencyLinks: {
diff --git a/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js b/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js
index a32e76a132e..f942a334f40 100644
--- a/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js
@@ -15,8 +15,8 @@ import InstallationCommands from '~/packages_and_registries/package_registry/com
import PackageFiles from '~/packages_and_registries/package_registry/components/details/package_files.vue';
import PackageHistory from '~/packages_and_registries/package_registry/components/details/package_history.vue';
import PackageTitle from '~/packages_and_registries/package_registry/components/details/package_title.vue';
-import VersionRow from '~/packages_and_registries/package_registry/components/details/version_row.vue';
import DeletePackage from '~/packages_and_registries/package_registry/components/functional/delete_package.vue';
+import PackageVersionsList from '~/packages_and_registries/package_registry/components/details/package_versions_list.vue';
import {
FETCH_PACKAGE_DETAILS_ERROR_MESSAGE,
PACKAGE_TYPE_COMPOSER,
@@ -99,6 +99,7 @@ describe('PackagesApp', () => {
GlSprintf,
GlTabs,
GlTab,
+ PackageVersionsList,
},
mocks: {
$route: {
@@ -120,8 +121,7 @@ describe('PackagesApp', () => {
const findPackageFiles = () => wrapper.findComponent(PackageFiles);
const findDeleteFileModal = () => wrapper.findByTestId('delete-file-modal');
const findDeleteFilesModal = () => wrapper.findByTestId('delete-files-modal');
- const findVersionRows = () => wrapper.findAllComponents(VersionRow);
- const noVersionsMessage = () => wrapper.findByTestId('no-versions-message');
+ const findVersionsList = () => wrapper.findComponent(PackageVersionsList);
const findDependenciesCountBadge = () => wrapper.findComponent(GlBadge);
const findNoDependenciesMessage = () => wrapper.findByTestId('no-dependencies-message');
const findDependencyRows = () => wrapper.findAllComponents(DependencyRow);
@@ -558,38 +558,23 @@ describe('PackagesApp', () => {
});
describe('versions', () => {
- it('displays the correct version count when the package has versions', async () => {
+ it('displays versions list when the package has versions', async () => {
createComponent();
await waitForPromises();
- expect(findVersionRows()).toHaveLength(packageVersions().length);
+ expect(findVersionsList()).toBeDefined();
});
it('binds the correct props', async () => {
- const [versionPackage] = packageVersions();
- // eslint-disable-next-line no-underscore-dangle
- delete versionPackage.__typename;
- delete versionPackage.tags;
-
- createComponent();
-
+ const versionNodes = packageVersions();
+ createComponent({ packageEntity: { versions: { nodes: versionNodes } } });
await waitForPromises();
- expect(findVersionRows().at(0).props()).toMatchObject({
- packageEntity: expect.objectContaining(versionPackage),
+ expect(findVersionsList().props()).toMatchObject({
+ versions: expect.arrayContaining(versionNodes),
});
});
-
- it('displays the no versions message when there are none', async () => {
- createComponent({
- resolver: jest.fn().mockResolvedValue(packageDetailsQuery({ versions: { nodes: [] } })),
- });
-
- await waitForPromises();
-
- expect(noVersionsMessage().exists()).toBe(true);
- });
});
describe('dependency links', () => {
it('does not show the dependency links for a non nuget package', async () => {
diff --git a/spec/helpers/diff_helper_spec.rb b/spec/helpers/diff_helper_spec.rb
index 10931261c4f..78c0d0a2b11 100644
--- a/spec/helpers/diff_helper_spec.rb
+++ b/spec/helpers/diff_helper_spec.rb
@@ -539,4 +539,42 @@ RSpec.describe DiffHelper do
end
end
end
+
+ describe '#show_only_context_commits?' do
+ let(:params) { {} }
+ let(:merge_request) { build_stubbed(:merge_request) }
+ let(:has_no_commits) { true }
+
+ subject(:result) { helper.show_only_context_commits? }
+
+ before do
+ assign(:merge_request, merge_request)
+ allow(helper).to receive(:params).and_return(params)
+ allow(merge_request).to receive(:has_no_commits?).and_return(has_no_commits)
+ end
+
+ context 'when only_context_commits param is set to true' do
+ let(:params) { { only_context_commits: true } }
+
+ it { is_expected.to be_truthy }
+
+ context 'when merge request has commits' do
+ let(:has_no_commits) { false }
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ context 'when only_context_commits param is set to false' do
+ let(:params) { { only_context_commits: false } }
+
+ it { is_expected.to be_truthy }
+
+ context 'when merge request has commits' do
+ let(:has_no_commits) { false }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+ end
end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 235342b00c9..9d169c50013 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -78,7 +78,6 @@ require_relative '../tooling/quality/test_level'
quality_level = Quality::TestLevel.new
RSpec.configure do |config|
- config.threadsafe = false
config.use_transactional_fixtures = true
config.use_instantiated_fixtures = false
config.fixture_path = Rails.root
diff --git a/spec/support/rspec.rb b/spec/support/rspec.rb
index 6795d2f6d2a..71dfc3fd5a3 100644
--- a/spec/support/rspec.rb
+++ b/spec/support/rspec.rb
@@ -11,6 +11,9 @@ require_relative "helpers/fast_rails_root"
RSpec::Expectations.configuration.on_potential_false_positives = :raise
RSpec.configure do |config|
+ # See https://gitlab.com/gitlab-org/gitlab/-/issues/379686
+ config.threadsafe = false
+
# Re-run failures locally with `--only-failures`
config.example_status_persistence_file_path = ENV.fetch('RSPEC_LAST_RUN_RESULTS_FILE', './spec/examples.txt')