Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2025-03-07 06:07:25 +00:00
parent f850f5c033
commit 0fc7ea6bc8
34 changed files with 558 additions and 80 deletions

View File

@ -1 +1 @@
4d333b2bb343d59410a80873e6185b8f8a2061a3
6bd8110489a44adeacff1897a757d8ea52bf4f87

View File

@ -23,6 +23,7 @@ import { FIND_FILE_BUTTON_CLICK } from '~/tracking/constants';
import { updateElementsVisibility } from '~/repository/utils/dom';
import blobControlsQuery from '~/repository/queries/blob_controls.query.graphql';
import { getRefType } from '~/repository/utils/ref_type';
import OpenMrBadge from '~/repository/components/header_area/open_mr_badge.vue';
import { TEXT_FILE_TYPE, DEFAULT_BLOB_INFO } from '../../constants';
import OverflowMenu from './blob_overflow_menu.vue';
@ -36,6 +37,7 @@ export default {
},
buttonClassList: 'sm:gl-w-auto gl-w-full sm:gl-mt-0 gl-mt-3',
components: {
OpenMrBadge,
GlButton,
OverflowMenu,
},
@ -192,6 +194,11 @@ export default {
</script>
<template>
<div v-if="showBlobControls" class="gl-flex gl-flex-wrap gl-items-center gl-gap-3">
<open-mr-badge
v-if="glFeatures.filterBlobPath"
:project-path="projectPath"
:blob-path="filePath"
/>
<gl-button
v-gl-tooltip.html="findFileTooltip"
:aria-keyshortcuts="findFileShortcutKey"

View File

@ -126,6 +126,7 @@ export default {
data-testid="default-actions-container"
:toggle-text="$options.i18n.dropdownLabel"
text-sr-only
category="tertiary"
>
<permalink-dropdown-item :permalink-path="blobInfo.permalinkPath" />
<blob-button-group

View File

@ -0,0 +1,80 @@
<script>
import { GlBadge } from '@gitlab/ui';
import { sprintf, __ } from '~/locale';
import getOpenMrCountsForBlobPath from '~/repository/queries/open_mr_counts.query.graphql';
import { nDaysBefore } from '~/lib/utils/datetime/date_calculation_utility';
import { toYmd } from '~/analytics/shared/utils';
import { logError } from '~/lib/logger';
import * as Sentry from '~/sentry/sentry_browser_wrapper';
const OPEN_MR_AGE_LIMIT_DAYS = 30;
export default {
components: {
GlBadge,
},
inject: ['currentRef'],
props: {
projectPath: {
type: String,
required: true,
},
blobPath: {
type: String,
required: true,
},
},
data() {
return {
openMrsCount: null,
};
},
computed: {
openMRsCountText() {
return sprintf(__('%{count} open'), { count: this.openMrsCount });
},
createdAfter() {
const lookbackDate = nDaysBefore(new Date(), OPEN_MR_AGE_LIMIT_DAYS - 1, { utc: true });
return toYmd(lookbackDate);
},
isLoading() {
return this.$apollo.queries.loading;
},
showBadge() {
return !this.isLoading && this.openMrsCount > 0;
},
queryVariables() {
return {
projectPath: this.projectPath,
targetBranch: [this.currentRef],
blobPath: this.blobPath,
createdAfter: this.createdAfter,
};
},
},
apollo: {
openMrsCount: {
query: getOpenMrCountsForBlobPath,
variables() {
return this.queryVariables;
},
update({ project: { mergeRequests: { count } = {} } = {} } = {}) {
return count;
},
error(error) {
logError(
`Failed to fetch merge request count. See exception details for more information.`,
error,
);
Sentry.captureException(error);
},
},
},
};
</script>
<template>
<gl-badge v-if="showBadge" variant="success" icon="merge-request">
{{ openMRsCountText }}
</gl-badge>
</template>

View File

@ -0,0 +1,18 @@
query getOpenMrCountsForBlobPath(
$projectPath: ID!
$targetBranch: [String!]
$blobPath: String!
$createdAfter: Time!
) {
project(fullPath: $projectPath) {
id
mergeRequests(
state: opened
targetBranches: $targetBranch
blobPath: $blobPath
createdAfter: $createdAfter
) {
count
}
}
}

View File

@ -107,7 +107,7 @@ export default {
return this.getNoteableData.noteableType === 'MergeRequest';
},
iconBgClass() {
return ICON_COLORS[this.note.system_note_icon_name] || 'gl-bg-strong gl-text-subtle';
return ICON_COLORS[this.note.system_note_icon_name] || 'gl-text-subtle';
},
systemNoteIconName() {
let icon = this.note.system_note_icon_name;
@ -160,7 +160,7 @@ export default {
iconBgClass,
{
'system-note-icon -gl-mt-1 gl-ml-2 gl-h-6 gl-w-6': isAllowedIcon,
'system-note-dot -gl-top-1 gl-ml-4 gl-mt-3 gl-h-3 gl-w-3 gl-border-2 gl-border-solid gl-border-subtle gl-bg-gray-900':
'system-note-dot -gl-top-1 gl-ml-4 gl-mt-3 gl-h-3 gl-w-3 gl-border-2 gl-border-solid gl-border-subtle':
!isAllowedIcon,
},
]"

View File

@ -26,9 +26,9 @@ import { __ } from '~/locale';
import NoteHeader from '~/notes/components/note_header.vue';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
const ALLOWED_ICONS = ['issue-close'];
const ICON_COLORS = {
'issue-close': '!gl-bg-blue-100 gl-text-blue-700 icon-info',
'issue-close': 'system-note-icon-info',
issues: 'system-note-icon-success',
};
export default {
@ -75,7 +75,7 @@ export default {
return ICON_COLORS[this.note.systemNoteIconName] || '';
},
isAllowedIcon() {
return ALLOWED_ICONS.includes(this.note.systemNoteIconName);
return Object.keys(ICON_COLORS).includes(this.note.systemNoteIconName);
},
isTargetNote() {
return this.targetNoteHash === this.noteAnchorId;
@ -101,6 +101,13 @@ export default {
deleteButtonClasses() {
return this.singleLineDescription ? 'gl-top-5 gl-right-2 gl-mt-2' : 'gl-top-6 gl-right-3';
},
systemNoteIconName() {
let icon = this.note.systemNoteIconName;
if (this.note.systemNoteIconName === 'issues') {
icon = 'issue-open-m';
}
return icon;
},
},
mounted() {
renderGFM(this.$refs['gfm-content']);
@ -126,13 +133,13 @@ export default {
getIconColor,
{
'system-note-icon -gl-mt-1 gl-ml-2 gl-h-6 gl-w-6': isAllowedIcon,
'system-note-dot -gl-top-1 gl-ml-4 gl-mt-3 gl-h-3 gl-w-3 gl-border-2 gl-border-solid gl-border-subtle gl-bg-gray-900':
'system-note-dot -gl-top-1 gl-ml-4 gl-mt-3 gl-h-3 gl-w-3 gl-border-2 gl-border-solid gl-border-subtle':
!isAllowedIcon,
},
]"
class="gl-relative gl-float-left gl-flex gl-items-center gl-justify-center gl-rounded-full"
>
<gl-icon v-if="isAllowedIcon" :size="14" :name="note.systemNoteIconName" />
<gl-icon v-if="isAllowedIcon" :size="14" :name="systemNoteIconName" />
</div>
<div class="gl-ml-7">
<div class="gl-flex gl-items-start gl-justify-between">

View File

@ -870,18 +870,17 @@ table.code {
}
}
.diff-file.linked-file .file-title {
background-color: $blue-50;
border-color: $blue-200;
}
.diff-file.linked-file {
border-color: var(--blue-200);
.diff-file.linked-file .diff-content {
border-color: $blue-200;
.file-title {
@apply gl-bg-feedback-info;
}
}
// This is not inside page_bundles because then it won't receive a proper color value for the dark theme
.diff-file-row.is-active.is-linked {
background-color: $blue-50;
@apply gl-bg-feedback-info;
}
.collapsed-file-warning {

View File

@ -455,7 +455,7 @@ span.idiff {
// The virtual scroller has a flat HTML structure so instead of the ::before
// element stretching over multiple rows we instead create a repeating background image
// for the line
background: repeating-linear-gradient(to right, var(--gray-100, $gray-100), var(--gray-100, $gray-100) 1px, transparent 1px, transparent 14px);
background: repeating-linear-gradient(to right, var(--gl-border-color-default), var(--gl-border-color-default) 1px, transparent 1px, transparent 14px);
background-size: calc(var(--level) * 14px) 100%;
background-repeat: no-repeat;
background-position: 14px;

View File

@ -364,11 +364,11 @@
}
.diff-file-row.is-active {
background-color: var(--gray-50, $gray-50);
@apply gl-bg-strong;
}
.diff-file-row.is-loading {
color: var(--gl-text-color-disabled);
@apply gl-text-disabled;
}
.mr-info-list {

View File

@ -46,6 +46,9 @@
}
}
.system-note-dot {
background-color: var(--gl-status-neutral-icon-color);
}
// Vertical line gradient for specific system note icons
.system-note-icon {
@ -53,22 +56,22 @@
background-color: var(--system-note-icon-background-color);
&.system-note-icon-success {
--system-note-icon-color: var(--gl-status-success-text-color);
--system-note-icon-color: var(--gl-status-success-icon-color);
--system-note-icon-background-color: var(--gl-status-success-background-color);
}
&.system-note-icon-danger {
--system-note-icon-color: var(--gl-status-danger-text-color);
--system-note-icon-color: var(--gl-status-danger-icon-color);
--system-note-icon-background-color: var(--gl-status-danger-background-color);
}
&.system-note-icon-info {
--system-note-icon-color: var(--gl-status-info-text-color);
--system-note-icon-color: var(--gl-status-info-icon-color);
--system-note-icon-background-color: var(--gl-status-info-background-color);
}
&.system-note-icon-warning {
--system-note-icon-color: var(--gl-status-warning-text-color);
--system-note-icon-color: var(--gl-status-warning-icon-color);
--system-note-icon-background-color: var(--gl-status-warning-background-color);
}

View File

@ -46,6 +46,7 @@ class Projects::BlobController < Projects::ApplicationController
before_action do
push_frontend_feature_flag(:inline_blame, @project)
push_frontend_feature_flag(:blob_overflow_menu, current_user)
push_frontend_feature_flag(:filter_blob_path, current_user)
push_frontend_feature_flag(:blob_repository_vue_header_app, @project)
push_licensed_feature(:file_locks) if @project.licensed_feature_available?(:file_locks)
push_frontend_feature_flag(:directory_code_dropdown_updates, current_user)

View File

@ -21,6 +21,7 @@ class Projects::TreeController < Projects::ApplicationController
push_frontend_feature_flag(:inline_blame, @project)
push_frontend_feature_flag(:blob_repository_vue_header_app, @project)
push_frontend_feature_flag(:blob_overflow_menu, current_user)
push_frontend_feature_flag(:filter_blob_path, current_user)
push_licensed_feature(:file_locks) if @project.licensed_feature_available?(:file_locks)
push_frontend_feature_flag(:directory_code_dropdown_updates, current_user)
end

View File

@ -44,6 +44,7 @@ class ProjectsController < Projects::ApplicationController
push_frontend_feature_flag(:page_specific_styles, current_user)
push_frontend_feature_flag(:blob_repository_vue_header_app, @project)
push_frontend_feature_flag(:blob_overflow_menu, current_user)
push_frontend_feature_flag(:filter_blob_path, current_user)
push_licensed_feature(:file_locks) if @project.present? && @project.licensed_feature_available?(:file_locks)
push_frontend_feature_flag(:directory_code_dropdown_updates, current_user)

View File

@ -32,6 +32,15 @@ module Resolvers
"for example: `id_desc` or `name_asc`",
default_value: 'name_asc'
argument :all_available, GraphQL::Types::Boolean,
required: false,
default_value: true,
replace_null_with_default: true,
description: <<~DESC
When `true`, returns all accessible groups. When `false`, returns only groups where the user is a member.
Unauthenticated requests always return all public groups. The `owned_only` argument takes precedence.
DESC
private
def resolve_groups(**args)

View File

@ -36,12 +36,18 @@ module Ci
def policies_allowed?(accessed_project, policies)
return true if self_referential?(accessed_project)
unless accessed_project.ci_inbound_job_token_scope_enabled?
# We capture policies even if the inbound scopes are disabled
capture_job_token_policies(policies)
return true
end
return false unless inbound_accessible?(accessed_project)
# We capture policies even if the inbound scopes are disabled or the feature flag is disabled
Ci::JobToken::Authorization.capture_job_token_policies(policies) if policies.present?
# We capture policies even if the feature flag is disabled
capture_job_token_policies(policies)
return true unless accessed_project.ci_inbound_job_token_scope_enabled?
return true unless Feature.enabled?(:add_policies_to_ci_job_token, accessed_project)
policies_allowed_for_accessed_project?(accessed_project, policies)
@ -140,6 +146,12 @@ module Ci
def outbound_allowlist
Ci::JobToken::Allowlist.new(current_project, direction: :outbound)
end
def capture_job_token_policies(policies)
return if policies.blank?
Ci::JobToken::Authorization.capture_job_token_policies(policies)
end
end
end
end

View File

@ -233,17 +233,14 @@ module MergeRequests
def create_pipeline_for(merge_request, user, async: false, allow_duplicate: false)
create_pipeline_params = params.slice(:push_options).merge(allow_duplicate: allow_duplicate)
service = MergeRequests::CreatePipelineService.new(
project: project, current_user: user, params: create_pipeline_params
)
if async
MergeRequests::CreatePipelineWorker.perform_async(
project.id,
user.id,
merge_request.id,
create_pipeline_params.deep_stringify_keys)
service.execute_async(merge_request)
else
MergeRequests::CreatePipelineService
.new(project: project, current_user: user, params: create_pipeline_params)
.execute(merge_request)
service.execute(merge_request)
end
end

View File

@ -17,7 +17,7 @@ module MergeRequests
::MergeRequests::CreatePipelineWorker.perform_async(
project.id, current_user.id, merge_request.id,
params.merge(pipeline_creation_request: pipeline_creation_request)
params.merge(pipeline_creation_request: pipeline_creation_request).deep_stringify_keys
)
end

View File

@ -1,6 +1,6 @@
- @with_header = true
- page_classes = page_class.push(@html_class, user_application_color_mode).flatten.compact
- body_classes = [system_message_class, @body_class]
- body_classes = [system_message_class, @body_class, content_for(:body_class)]
!!! 5
%html.gl-h-full{ lang: I18n.locale, class: page_classes }

View File

@ -1,7 +1,5 @@
# frozen_string_literal: true
require 'irb'
if Gitlab::Runtime.console?
# Stop irb from writing a history file by default.
module IrbNoHistory

View File

@ -170,6 +170,148 @@ which causes the worker to process `unknown` artifacts
[in batches that are five times larger](https://gitlab.com/gitlab-org/gitlab/-/issues/356319).
This flag is not recommended for use.
#### `@final` artifacts not deleted from object store
An issue in GitLab 16.1 and 16.2 caused [`@final` artifacts to not be deleted from object storage](https://gitlab.com/gitlab-org/gitlab/-/issues/419920).
Administrators of GitLab instances that ran GitLab 16.1 or 16.2 for some time could see an increase
in object storage used by artifacts. Follow this procedure to check for and remove these artifacts.
Removing the files is a two stage process:
1. [Identify which files have been orphaned](#list-orphaned-job-artifacts).
1. [Delete the identified files from object storage](#delete-orphaned-job-artifacts).
##### List orphaned job artifacts
{{< tabs >}}
{{< tab title="Linux package (Omnibus)" >}}
```shell
sudo gitlab-rake gitlab:cleanup:list_orphan_job_artifact_final_objects
```
{{< /tab >}}
{{< tab title="Docker" >}}
```shell
docker exec -it <container-id> bash
gitlab-rake gitlab:cleanup:list_orphan_job_artifact_final_objects
```
Either write to a persistent volume mounted in the container, or when the command completes: copy the output file out of the session.
{{< /tab >}}
{{< tab title="Self-compiled (source)" >}}
```shell
sudo -u git -H bundle exec rake gitlab:cleanup:list_orphan_job_artifact_final_objects RAILS_ENV=production
```
{{< /tab >}}
{{< tab title="Helm chart (Kubernetes)" >}}
```shell
# find the pod
kubectl get pods --namespace <namespace> -lapp=toolbox
# open the Rails console
kubectl exec -it -c toolbox <toolbox-pod-name> bash
gitlab-rake gitlab:cleanup:list_orphan_job_artifact_final_objects
```
When the command complete, copy the file out of the session onto persistent storage.
{{< /tab >}}
{{< /tabs >}}
The Rake task has some additional features that apply to all types of GitLab deployment:
- Scanning object storage can be interrupted. Progress is recorded in Redis, this is used to resume
scanning artifacts from that point.
- By default, the Rake task generates a CSV file:
`/opt/gitlab/embedded/service/gitlab-rails/tmp/orphan_job_artifact_final_objects.csv`
- Set an environment variable to specify a different filename:
```shell
# Packaged GitLab
sudo su -
FILENAME='custom_filename.csv' gitlab-rake gitlab:cleanup:list_orphan_job_artifact_final_objects
```
- If the output file exists already (the default, or the specified file) it appends entries to the file.
- Each row contains the fields `object_path,object_size` comma separated, with no file header. For example:
```plaintext
35/13/35135aaa6cc23891b40cb3f378c53a17a1127210ce60e125ccf03efcfdaec458/@final/1a/1a/5abfa4ec66f1cc3b681a4d430b8b04596cbd636f13cdff44277211778f26,201
```
##### Delete orphaned job artifacts
{{< tabs >}}
{{< tab title="Linux package (Omnibus)" >}}
```shell
sudo gitlab-rake gitlab:cleanup:delete_orphan_job_artifact_final_objects
```
{{< /tab >}}
{{< tab title="Docker" >}}
```shell
docker exec -it <container-id> bash
gitlab-rake gitlab:cleanup:delete_orphan_job_artifact_final_objects
```
- Copy the output file out of the session when the command completes, or write it to a volume that has been mounted by the container.
{{< /tab >}}
{{< tab title="Self-compiled (source)" >}}
```shell
sudo -u git -H bundle exec rake gitlab:cleanup:delete_orphan_job_artifact_final_objects RAILS_ENV=production
```
{{< /tab >}}
{{< tab title="Helm chart (Kubernetes)" >}}
```shell
# find the pod
kubectl get pods --namespace <namespace> -lapp=toolbox
# open the Rails console
kubectl exec -it -c toolbox <toolbox-pod-name> bash
gitlab-rake gitlab:cleanup:delete_orphan_job_artifact_final_objects
```
- When the command complete, copy the file out of the session onto persistent storage.
{{< /tab >}}
{{< /tabs >}}
The following applies to all types of GitLab deployment:
- Specify the input filename using the `FILENAME` variable. By default the script looks for:
`/opt/gitlab/embedded/service/gitlab-rails/tmp/orphan_job_artifact_final_objects.csv`
- As the script deletes files, it writes out a CSV file with the deleted files:
- the file is in the same directory as the input file
- the filename is prefixed with `deleted_from--`. For example: `deleted_from--orphan_job_artifact_final_objects.csv`.
- The rows in the file are: `object_path,object_size,object_generation/version`, for example:
```plaintext
35/13/35135aaa6cc23891b40cb3f378c53a17a1127210ce60e125ccf03efcfdaec458/@final/1a/1a/5abfa4ec66f1cc3b681a4d430b8b04596cbd636f13cdff44277211778f26,201,1711616743796587
```
### List projects and builds with artifacts with a specific expiration (or no expiration)
Using a [Rails console](../operations/rails_console.md), you can find projects that have job artifacts with either:

View File

@ -731,6 +731,7 @@ four standard [pagination arguments](#pagination-arguments):
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="querygroupsallavailable"></a>`allAvailable` | [`Boolean`](#boolean) | When `true`, returns all accessible groups. When `false`, returns only groups where the user is a member. Unauthenticated requests always return all public groups. The `owned_only` argument takes precedence. |
| <a id="querygroupsids"></a>`ids` | [`[ID!]`](#id) | Filter groups by IDs. |
| <a id="querygroupsmarkedfordeletionon"></a>`markedForDeletionOn` | [`Date`](#date) | Date when the group was marked for deletion. |
| <a id="querygroupsownedonly"></a>`ownedOnly` | [`Boolean`](#boolean) | Only include groups where the current user has an owner role. |
@ -32322,6 +32323,7 @@ four standard [pagination arguments](#pagination-arguments):
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="organizationgroupsallavailable"></a>`allAvailable` | [`Boolean`](#boolean) | When `true`, returns all accessible groups. When `false`, returns only groups where the user is a member. Unauthenticated requests always return all public groups. The `owned_only` argument takes precedence. |
| <a id="organizationgroupsids"></a>`ids` | [`[ID!]`](#id) | Filter groups by IDs. |
| <a id="organizationgroupsmarkedfordeletionon"></a>`markedForDeletionOn` | [`Date`](#date) | Date when the group was marked for deletion. |
| <a id="organizationgroupsownedonly"></a>`ownedOnly` | [`Boolean`](#boolean) | Only include groups where the current user has an owner role. |

View File

@ -299,7 +299,7 @@ Parameters:
| Attribute | Type | Required | Description |
|--------------------------|-------------------|----------|-------------|
| `skip_groups` | array of integers | no | Skip the group IDs passed |
| `all_available` | boolean | no | Show all the groups you have access to (defaults to `false` for authenticated users, `true` for administrators); Attributes `owned` and `min_access_level` have precedence |
| `all_available` | boolean | no | When `true`, returns all accessible groups. When `false`, returns only groups where the user is a member. Defaults to `false` for users, `true` for administrators. Unauthenticated requests always return all public groups. The `owned` and `min_access_level` attributes take precedence. |
| `search` | string | no | Return the list of authorized groups matching the search criteria |
| `order_by` | string | no | Order groups by `name`, `path`, `id`, or `similarity`. Default is `name` |
| `sort` | string | no | Order groups in `asc` or `desc` order. Default is `asc` |
@ -928,6 +928,7 @@ Parameters:
| `with_custom_attributes` | boolean | no | Include [custom attributes](custom_attributes.md) in response (administrators only) |
| `owned` | boolean | no | Limit to groups explicitly owned by the current user |
| `min_access_level` | integer | no | Limit to groups where current user has at least this [role (`access_level`)](members.md#roles) |
| `all_available` | boolean | no | When `true`, returns all accessible groups. When `false`, returns only groups where the user is a member. Defaults to `false` for users, `true` for administrators. Unauthenticated requests always return all public groups. The `owned` and `min_access_level` attributes take precedence. |
```plaintext
GET /groups/:id/subgroups
@ -997,7 +998,7 @@ Parameters:
| ------------------------ | ----------------- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](rest/_index.md#namespaced-paths) of the immediate parent group |
| `skip_groups` | array of integers | no | Skip the group IDs passed |
| `all_available` | boolean | no | Show all the groups you have access to (defaults to `false` for authenticated users, `true` for administrators). Attributes `owned` and `min_access_level` have precedence |
| `all_available` | boolean | no | When `true`, returns all accessible groups. When `false`, returns only groups where the user is a member. Defaults to `false` for users, `true` for administrators. Unauthenticated requests always return all public groups. The `owned` and `min_access_level` attributes take precedence. |
| `search` | string | no | Return the list of authorized groups matching the search criteria. Only descendant group short paths are searched (not full paths) |
| `order_by` | string | no | Order groups by `name`, `path`, or `id`. Default is `name` |
| `sort` | string | no | Order groups in `asc` or `desc` order. Default is `asc` |

View File

@ -114,6 +114,8 @@ For more information about our plans for language support in SAST, see the [cate
| Scala | {{< icon name="dotted-circle" >}} No; tracked in [epic 15174](https://gitlab.com/groups/gitlab-org/-/epics/15174) | {{< icon name="check-circle" >}} Yes: [Semgrep](https://gitlab.com/gitlab-org/security-products/analyzers/semgrep) with [GitLab-managed rules](rules.md#semgrep-based-analyzer) |
| Swift (iOS) | {{< icon name="dotted-circle" >}} No | {{< icon name="check-circle" >}} Yes: [Semgrep](https://gitlab.com/gitlab-org/security-products/analyzers/semgrep) with [GitLab-managed rules](rules.md#semgrep-based-analyzer) |
| TypeScript | {{< icon name="check-circle" >}} Yes | {{< icon name="check-circle" >}} Yes: [Semgrep](https://gitlab.com/gitlab-org/security-products/analyzers/semgrep) with [GitLab-managed rules](rules.md#semgrep-based-analyzer) |
| YAML | {{< icon name="check-circle" >}} Yes | {{< icon name="check-circle" >}} Yes: [Semgrep](https://gitlab.com/gitlab-org/security-products/analyzers/semgrep) with [GitLab-managed rules](rules.md#semgrep-based-analyzer) |
| Java Properties | {{< icon name="check-circle" >}} Yes | {{< icon name="check-circle" >}} Yes: [Semgrep](https://gitlab.com/gitlab-org/security-products/analyzers/semgrep) with [GitLab-managed rules](rules.md#semgrep-based-analyzer) |
**Footnotes:**
@ -819,7 +821,7 @@ flags are added to the scanner's CLI options.
<code>--multi-core</code>
</td>
<td>
Specify the number of CPU cores to utilize for scanning. This can significantly improve scan performance on multi-core systems. When setting this value, ensure that the number of cores specified does not exceed the total number of cores available to the container. Note that multi-core execution will require proportionally more memory than single-core execution. Exceeding the available cores or memory resources may lead to resource contention and suboptimal performance. Defaults to <code>1</code>.
Multi-core scanning is enabled by default, automatically detecting and utilizing available CPU cores based on container information. On self-hosted runners, the maximum number of cores is capped at 4. You can override the automatic core detection by explicitly setting <code>--multi-core</code> to a specific value. Multi-core execution requires proportionally more memory than single-core execution. To disable multi-core scanning, set the environment variable <code>DISABLE_MULTI_CORE</code>. Note that exceeding available cores or memory resources may lead to resource contention and suboptimal performance.
</td>
</tr>
<tr>

View File

@ -22,7 +22,7 @@ module API
params :group_list_params do
use :statistics_params
optional :skip_groups, type: Array[Integer], coerce_with: ::API::Validations::Types::CommaSeparatedToIntegerArray.coerce, desc: 'Array of group ids to exclude from list'
optional :all_available, type: Boolean, desc: 'Show all group that you have access to'
optional :all_available, type: Boolean, desc: 'When `true`, returns all accessible groups. When `false`, returns only groups where the user is a member.'
optional :visibility, type: String, values: Gitlab::VisibilityLevel.string_values,
desc: 'Limit by visibility'
optional :search, type: String, desc: 'Search for a specific group'

View File

@ -785,6 +785,9 @@ msgstr ""
msgid "%{count} of %{total}"
msgstr ""
msgid "%{count} open"
msgstr ""
msgid "%{count} project"
msgid_plural "%{count} projects"
msgstr[0] ""
@ -21932,10 +21935,10 @@ msgstr ""
msgid "DuoEnterpriseTrial|Gain deeper insights into GitLab Duo usage patterns"
msgstr ""
msgid "DuoEnterpriseTrial|GitLab Duo Enterprise is is your end-to-end AI partner for faster, more secure software development."
msgid "DuoEnterpriseTrial|GitLab Duo Enterprise is only available for purchase for Ultimate customers."
msgstr ""
msgid "DuoEnterpriseTrial|GitLab Duo Enterprise is only available for purchase for Ultimate customers."
msgid "DuoEnterpriseTrial|GitLab Duo Enterprise is your end-to-end AI partner for faster, more secure software development."
msgstr ""
msgid "DuoEnterpriseTrial|Maintain control and keep your data safe"

View File

@ -14,6 +14,7 @@ import Shortcuts from '~/behaviors/shortcuts/shortcuts';
import BlobLinePermalinkUpdater from '~/blob/blob_line_permalink_updater';
import OverflowMenu from '~/repository/components/header_area/blob_overflow_menu.vue';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import OpenMrBadge from '~/repository/components/header_area/open_mr_badge.vue';
import { blobControlsDataMock, refMock } from '../../mock_data';
jest.mock('~/repository/utils/dom');
@ -24,17 +25,22 @@ let router;
let wrapper;
let mockResolver;
const createComponent = async (
const createComponent = async ({
props = {},
blobInfoOverrides = {},
glFeatures = { blobOverflowMenu: false },
) => {
routerOverride = {},
} = {}) => {
Vue.use(VueApollo);
const projectPath = 'some/project';
router = createRouter(projectPath, refMock);
await router.push({ name: 'blobPathDecoded', params: { path: '/some/file.js' } });
await router.push({
name: 'blobPathDecoded',
params: { path: '/some/file.js' },
...routerOverride,
});
mockResolver = jest.fn().mockResolvedValue({
data: {
@ -75,6 +81,7 @@ const createComponent = async (
};
describe('Blob controls component', () => {
const findOpenMrBadge = () => wrapper.findComponent(OpenMrBadge);
const findFindButton = () => wrapper.findByTestId('find');
const findBlameButton = () => wrapper.findByTestId('blame');
const findPermalinkButton = () => wrapper.findByTestId('permalink');
@ -85,6 +92,36 @@ describe('Blob controls component', () => {
await createComponent();
});
describe('MR badge', () => {
it('should render the baadge if `filter_blob_path` flag is on', async () => {
await createComponent({ glFeatures: { filterBlobPath: true } });
expect(findOpenMrBadge().exists()).toBe(true);
expect(findOpenMrBadge().props('blobPath')).toBe('/some/file.js');
expect(findOpenMrBadge().props('projectPath')).toBe('some/project');
});
it('should not render the baadge if `filter_blob_path` flag is off', async () => {
await createComponent({ glFeatures: { filterBlobPath: false } });
expect(findOpenMrBadge().exists()).toBe(false);
});
});
describe('showBlobControls', () => {
it('should not render blob controls when filePath does not exist', async () => {
await createComponent({
routerOverride: { name: 'blobPathDecoded', params: null },
});
expect(wrapper.element).not.toBeVisible();
});
it('should not render blob controls when route name is not blobPathDecoded', async () => {
await createComponent({
routerOverride: { name: 'blobPath', params: { path: '/some/file.js' } },
});
expect(wrapper.element).not.toBeVisible();
});
});
describe('FindFile button', () => {
it('renders FindFile button', () => {
expect(findFindButton().exists()).toBe(true);
@ -114,13 +151,15 @@ describe('Blob controls component', () => {
});
it('does not render blame button when blobInfo.storedExternally is true', async () => {
await createComponent({}, { storedExternally: true });
await createComponent({ blobInfoOverrides: { storedExternally: true } });
expect(findBlameButton().exists()).toBe(false);
});
it('does not render blame button when blobInfo.externalStorage is "lfs"', async () => {
await createComponent({}, { storedExternally: true, externalStorage: 'lfs' });
await createComponent({
blobInfoOverrides: { storedExternally: true, externalStorage: 'lfs' },
});
expect(findBlameButton().exists()).toBe(false);
});
@ -164,7 +203,7 @@ describe('Blob controls component', () => {
describe('BlobOverflow dropdown', () => {
it('renders BlobOverflow component with correct props', async () => {
await createComponent({}, {}, { blobOverflowMenu: true });
await createComponent({ glFeatures: { blobOverflowMenu: true } });
expect(findOverflowMenu().exists()).toBe(true);
expect(findOverflowMenu().props()).toEqual({
@ -177,22 +216,26 @@ describe('Blob controls component', () => {
});
it('passes the correct isBinary value to BlobOverflow when viewing a binary file', async () => {
await createComponent(
{ isBinary: true },
{
await createComponent({
props: {
isBinary: true,
},
blobInfoOverrides: {
simpleViewer: {
...blobControlsDataMock.repository.blobs.nodes[0].simpleViewer,
fileType: 'podfile',
},
},
{ blobOverflowMenu: true },
);
glFeatures: {
blobOverflowMenu: true,
},
});
expect(findOverflowMenu().props('isBinary')).toBe(true);
});
it('copies to clipboard raw blob text, when receives copy event', async () => {
await createComponent({}, {}, { blobOverflowMenu: true });
await createComponent({ glFeatures: { blobOverflowMenu: true } });
jest.spyOn(navigator.clipboard, 'writeText');
findOverflowMenu().vm.$emit('copy');

View File

@ -0,0 +1,23 @@
export const openMRQueryResult = jest.fn().mockResolvedValue({
data: {
project: {
__typename: 'Project',
id: '1234',
mergeRequests: {
count: 3,
},
},
},
});
export const zeroOpenMRQueryResult = jest.fn().mockResolvedValue({
data: {
project: {
__typename: 'Project',
id: '1234',
mergeRequests: {
count: 0,
},
},
},
});

View File

@ -0,0 +1,99 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { shallowMount } from '@vue/test-utils';
import { GlBadge } from '@gitlab/ui';
import OpenMrBadge from '~/repository/components/header_area/open_mr_badge.vue';
import getOpenMrCountsForBlobPath from '~/repository/queries/open_mr_counts.query.graphql';
import { logError } from '~/lib/logger';
import waitForPromises from 'helpers/wait_for_promises';
import createMockApollo from 'helpers/mock_apollo_helper';
import { useFakeDate } from 'helpers/fake_date';
import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { openMRQueryResult, zeroOpenMRQueryResult } from './mock_data';
Vue.use(VueApollo);
jest.mock('~/lib/logger');
jest.mock('~/sentry/sentry_browser_wrapper');
describe('OpenMrBadge', () => {
let wrapper;
let requestHandler;
const defaultProps = {
projectPath: 'group/project',
blobPath: 'path/to/file.js',
};
function createComponent(props = {}, mockResolver = openMRQueryResult) {
requestHandler = mockResolver;
const mockApollo = createMockApollo([[getOpenMrCountsForBlobPath, mockResolver]]);
wrapper = shallowMount(OpenMrBadge, {
propsData: {
...defaultProps,
...props,
},
provide: {
currentRef: 'main',
},
apolloProvider: mockApollo,
});
}
describe('rendering', () => {
it('does not render badge when query is loading', () => {
createComponent();
expect(wrapper.findComponent(GlBadge).exists()).toBe(false);
});
it('does not render badge when there are no open MRs', async () => {
createComponent({}, zeroOpenMRQueryResult);
await waitForPromises();
expect(wrapper.findComponent(GlBadge).exists()).toBe(false);
});
it('renders badge when when there are open MRs', async () => {
createComponent();
await waitForPromises();
const badge = wrapper.findComponent(GlBadge);
expect(badge.exists()).toBe(true);
expect(badge.props('variant')).toBe('success');
expect(badge.props('icon')).toBe('merge-request');
expect(wrapper.text()).toBe('3 open');
});
});
describe('computed properties', () => {
useFakeDate();
beforeEach(() => {
createComponent({});
});
it('computes queryVariables correctly', () => {
expect(requestHandler).toHaveBeenCalledWith({
blobPath: 'path/to/file.js',
createdAfter: '2020-06-07',
projectPath: 'group/project',
targetBranch: ['main'],
});
});
});
describe('apollo query', () => {
it('handles apollo error correctly', async () => {
const mockError = new Error();
createComponent({}, jest.fn().mockRejectedValueOnce(mockError));
await waitForPromises();
expect(wrapper.findComponent(GlBadge).exists()).toBe(false);
expect(logError).toHaveBeenCalledWith(
'Failed to fetch merge request count. See exception details for more information.',
mockError,
);
expect(Sentry.captureException).toHaveBeenCalledWith(mockError);
});
});
});

View File

@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe Resolvers::GroupsResolver, feature_category: :groups_and_projects do
using RSpec::Parameterized::TableSyntax
include GraphqlHelpers
describe '#resolve' do
@ -75,5 +77,23 @@ RSpec.describe Resolvers::GroupsResolver, feature_category: :groups_and_projects
end
end
end
context 'with `all_available` argument' do
where :args, :expected_param do
{} | { all_available: true }
{ all_available: nil } | { all_available: true }
{ all_available: true } | { all_available: true }
{ all_available: false } | { all_available: false }
end
with_them do
it 'pass the correct parameter to the GroupsFinder' do
expect(GroupsFinder).to receive(:new)
.with(user, hash_including(**expected_param)).and_call_original
resolve(described_class, args: args, ctx: { current_user: user })
end
end
end
end
end

View File

@ -109,30 +109,32 @@ RSpec.describe MergeRequests::BaseService, feature_category: :code_review_workfl
end
end
context 'async: true' do
it 'enques a CreatePipelineWorker' do
expect(MergeRequests::CreatePipelineService).not_to receive(:new)
expect(MergeRequests::CreatePipelineWorker)
.to receive(:perform_async)
.with(project.id, user.id, merge_request.id, { "allow_duplicate" => false })
.and_call_original
context 'when async: true' do
it 'executes MergeRequests::CreatePipelineService async' do
service = instance_double(MergeRequests::CreatePipelineService)
Sidekiq::Testing.fake! do
expect { subject.execute(merge_request, async: true) }.to change(MergeRequests::CreatePipelineWorker.jobs, :size).by(1)
end
expect(MergeRequests::CreatePipelineService)
.to receive(:new)
.with(project: project, current_user: user, params: { allow_duplicate: false })
.and_return(service)
expect(service).to receive(:execute_async).with(merge_request)
subject.execute(merge_request, async: true)
end
context 'allow_duplicate: true' do
context 'when allow_duplicate: true' do
it 'passes :allow_duplicate as true' do
expect(MergeRequests::CreatePipelineService).not_to receive(:new)
expect(MergeRequests::CreatePipelineWorker)
.to receive(:perform_async)
.with(project.id, user.id, merge_request.id, { "allow_duplicate" => true })
.and_call_original
service = instance_double(MergeRequests::CreatePipelineService)
Sidekiq::Testing.fake! do
expect { subject.execute(merge_request, async: true, allow_duplicate: true) }.to change(MergeRequests::CreatePipelineWorker.jobs, :size).by(1)
end
expect(MergeRequests::CreatePipelineService)
.to receive(:new)
.with(project: project, current_user: user, params: { allow_duplicate: true })
.and_return(service)
expect(service).to receive(:execute_async).with(merge_request)
subject.execute(merge_request, async: true, allow_duplicate: true)
end
end
end

View File

@ -301,7 +301,9 @@ RSpec.describe MergeRequests::CreatePipelineService, :clean_gitlab_redis_cache,
describe '#execute_async' do
it 'queues a merge request pipeline creation' do
expect(MergeRequests::CreatePipelineWorker).to receive(:perform_async)
expect(MergeRequests::CreatePipelineWorker).to receive(:perform_async).with(
project.id, user.id, merge_request.id, { 'pipeline_creation_request' => anything }
)
expect(GraphqlTriggers).to receive(:merge_request_merge_status_updated).with(merge_request)
service.execute_async(merge_request)

View File

@ -120,8 +120,8 @@ RSpec.describe MergeRequests::RefreshService, feature_category: :code_review_wor
end
it 'triggers mergeRequestMergeStatusUpdated GraphQL subscription conditionally' do
expect(GraphqlTriggers).to receive(:merge_request_merge_status_updated).with(@merge_request)
expect(GraphqlTriggers).to receive(:merge_request_merge_status_updated).with(@another_merge_request)
expect(GraphqlTriggers).to receive(:merge_request_merge_status_updated).twice.with(@merge_request)
expect(GraphqlTriggers).to receive(:merge_request_merge_status_updated).twice.with(@another_merge_request)
expect(GraphqlTriggers).not_to receive(:merge_request_merge_status_updated).with(@fork_merge_request)
refresh_service.execute(@oldrev, @newrev, 'refs/heads/master')

View File

@ -495,9 +495,14 @@ RSpec.describe MergeRequests::UpdateService, :mailer, feature_category: :code_re
shared_examples_for "creates a new pipeline" do
it "creates a new pipeline" do
expect(MergeRequests::CreatePipelineWorker)
.to receive(:perform_async)
.with(project.id, user.id, merge_request.id, { "allow_duplicate" => true })
service = instance_double(MergeRequests::CreatePipelineService)
expect(MergeRequests::CreatePipelineService)
.to receive(:new)
.with(project: project, current_user: user, params: { allow_duplicate: true })
.and_return(service)
expect(service).to receive(:execute_async).with(merge_request)
update_merge_request(target_branch: new_target_branch)
end